Compare commits
15 Commits
721c9b4e71
...
v0.1.3
Author | SHA1 | Date | |
---|---|---|---|
f52249c292
|
|||
aa70183d75
|
|||
4ad1d3edf6
|
|||
55bf725491
|
|||
13f1785081
|
|||
bb6d51a602
|
|||
a4114c5be3
|
|||
5c13dc0a1c
|
|||
cef0675a88
|
|||
fd223290df
|
|||
5f4382911b
|
|||
95cee69482
|
|||
33c166fa0c
|
|||
36863e4d9f
|
|||
fb0e5107cf
|
@@ -37,7 +37,15 @@
|
||||
"Bash(rg:*)",
|
||||
"Bash(../target/release/ailog build)",
|
||||
"Bash(zsh run.zsh:*)",
|
||||
"Bash(hugo:*)"
|
||||
"Bash(hugo:*)",
|
||||
"WebFetch(domain:docs.bsky.app)",
|
||||
"WebFetch(domain:syui.ai)",
|
||||
"Bash(rustup target list:*)",
|
||||
"Bash(rustup target:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git tag:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
53
.gitea/workflows/deploy.yml
Normal file
53
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Deploy to Cloudflare Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Build ailog
|
||||
run: |
|
||||
cargo build --release
|
||||
|
||||
- name: Build OAuth app
|
||||
run: |
|
||||
cd oauth
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Copy OAuth assets
|
||||
run: |
|
||||
cp -r oauth/dist/* my-blog/static/
|
||||
|
||||
- name: Generate site with ailog
|
||||
run: |
|
||||
./target/release/ailog generate --input content --output my-blog/public
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: syui-ai
|
||||
directory: my-blog/public
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
28
.gitea/workflows/example-usage.yml
Normal file
28
.gitea/workflows/example-usage.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Example ailog usage
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger for testing
|
||||
|
||||
jobs:
|
||||
build-with-ailog-action:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build with ailog action
|
||||
uses: ai/log@v1 # This will reference this repository
|
||||
with:
|
||||
content-dir: 'content'
|
||||
output-dir: 'public'
|
||||
ai-integration: true
|
||||
atproto-integration: true
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: my-blog
|
||||
directory: public
|
51
.github/workflows/build-binary.yml
vendored
Normal file
51
.github/workflows/build-binary.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
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
|
77
.github/workflows/gh-pages-fast.yml
vendored
Normal file
77
.github/workflows/gh-pages-fast.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
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
|
169
.github/workflows/release.yml
vendored
Normal file
169
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g., v1.0.0)'
|
||||
required: true
|
||||
default: 'v0.1.0'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
OPENSSL_STATIC: true
|
||||
OPENSSL_VENDOR: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
artifact_name: ailog
|
||||
asset_name: ailog-linux-x86_64
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
artifact_name: ailog
|
||||
asset_name: ailog-linux-aarch64
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
artifact_name: ailog
|
||||
asset_name: ailog-macos-x86_64
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
artifact_name: ailog
|
||||
asset_name: ailog-macos-aarch64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install cross-compilation tools (Linux)
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
|
||||
|
||||
- name: Configure cross-compilation (Linux ARM64)
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
|
||||
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache target directory
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare binary
|
||||
shell: bash
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
|
||||
# Use appropriate strip command for cross-compilation
|
||||
if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]]; then
|
||||
aarch64-linux-gnu-strip ${{ matrix.artifact_name }} || echo "Strip failed, continuing..."
|
||||
elif [[ "${{ matrix.os }}" == "windows-latest" ]]; then
|
||||
strip ${{ matrix.artifact_name }} || echo "Strip failed, continuing..."
|
||||
else
|
||||
strip ${{ matrix.artifact_name }} || echo "Strip failed, continuing..."
|
||||
fi
|
||||
|
||||
# Create archive
|
||||
if [[ "${{ matrix.target }}" == *"windows"* ]]; then
|
||||
7z a ../../../${{ matrix.asset_name }}.zip ${{ matrix.artifact_name }}
|
||||
else
|
||||
tar czvf ../../../${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }}
|
||||
fi
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: ${{ matrix.asset_name }}.tar.gz
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
echo "## What's Changed" > release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "### Features" >> release_notes.md
|
||||
echo "- AI-powered static blog generator" >> release_notes.md
|
||||
echo "- AtProto OAuth integration" >> release_notes.md
|
||||
echo "- Automatic translation support" >> release_notes.md
|
||||
echo "- AI comment system" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "### Platforms" >> release_notes.md
|
||||
echo "- Linux (x86_64, aarch64)" >> release_notes.md
|
||||
echo "- macOS (Intel, Apple Silicon)" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "### Installation" >> release_notes.md
|
||||
echo "\`\`\`bash" >> release_notes.md
|
||||
echo "# Linux/macOS" >> release_notes.md
|
||||
echo "tar -xzf ailog-linux-x86_64.tar.gz" >> release_notes.md
|
||||
echo "chmod +x ailog" >> release_notes.md
|
||||
echo "sudo mv ailog /usr/local/bin/" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "\`\`\`" >> release_notes.md
|
||||
|
||||
- name: Get tag name
|
||||
id: tag_name
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.tag_name.outputs.tag }}
|
||||
name: ailog ${{ steps.tag_name.outputs.tag }}
|
||||
body_path: release_notes.md
|
||||
draft: false
|
||||
prerelease: ${{ contains(steps.tag_name.outputs.tag, 'alpha') || contains(steps.tag_name.outputs.tag, 'beta') || contains(steps.tag_name.outputs.tag, 'rc') }}
|
||||
files: artifacts/*/ailog-*.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ my-blog/public/
|
||||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
my-blog/static/assets/comment-atproto-*
|
||||
|
36
Cargo.toml
36
Cargo.toml
@@ -15,7 +15,7 @@ clap = { version = "4.5", features = ["derive"] }
|
||||
pulldown-cmark = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.40", features = ["full"] }
|
||||
tokio = { version = "1.40", features = ["rt-multi-thread", "macros", "fs", "net", "io-util", "sync", "time", "process", "signal"] }
|
||||
anyhow = "1.0"
|
||||
toml = "0.8"
|
||||
chrono = "0.4"
|
||||
@@ -26,7 +26,7 @@ fs_extra = "1.3"
|
||||
colored = "2.1"
|
||||
serde_yaml = "0.9"
|
||||
syntect = "5.2"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
rand = "0.8"
|
||||
sha2 = "0.10"
|
||||
base64 = "0.22"
|
||||
@@ -43,12 +43,38 @@ cookie = "0.18"
|
||||
syn = { version = "2.0", features = ["full", "parsing", "visit"] }
|
||||
quote = "1.0"
|
||||
ignore = "0.4"
|
||||
git2 = "0.18"
|
||||
git2 = { version = "0.18", features = ["vendored-openssl", "vendored-libgit2", "ssh"], default-features = false }
|
||||
regex = "1.0"
|
||||
# ATProto and stream monitoring dependencies
|
||||
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots", "connect"], default-features = false }
|
||||
futures-util = "0.3"
|
||||
tungstenite = { version = "0.21", features = ["native-tls"] }
|
||||
tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"], default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14"
|
||||
|
||||
[profile.dev]
|
||||
# Speed up development builds
|
||||
opt-level = 0
|
||||
debug = true
|
||||
debug-assertions = true
|
||||
overflow-checks = true
|
||||
lto = false
|
||||
panic = 'unwind'
|
||||
incremental = true
|
||||
codegen-units = 256
|
||||
|
||||
[profile.release]
|
||||
# Optimize release builds for speed and size
|
||||
opt-level = 3
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
codegen-units = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
# Optimize dependencies in dev builds
|
||||
opt-level = 3
|
84
README.md
84
README.md
@@ -2,6 +2,35 @@
|
||||
|
||||
AI-powered static blog generator with ATProto integration, part of the ai.ai ecosystem.
|
||||
|
||||
## 🎯 Gitea Action Usage
|
||||
|
||||
Use ailog in your Gitea Actions workflow:
|
||||
|
||||
```yaml
|
||||
name: Deploy Blog
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ai/log@v1
|
||||
with:
|
||||
content-dir: 'content'
|
||||
output-dir: 'public'
|
||||
ai-integration: true
|
||||
atproto-integration: true
|
||||
- uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: my-blog
|
||||
directory: public
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Development Setup
|
||||
@@ -13,14 +42,14 @@ cd log
|
||||
|
||||
# 2. Start development services
|
||||
./run.zsh serve # Blog development server
|
||||
./run.zsh c # Cloudflare tunnel (log.syui.ai)
|
||||
./run.zsh c # Cloudflare tunnel (example.com)
|
||||
./run.zsh o # OAuth web server
|
||||
./run.zsh co # Comment system monitor
|
||||
|
||||
# 3. Start Ollama (for Ask AI)
|
||||
brew install ollama
|
||||
ollama pull gemma2:2b
|
||||
OLLAMA_ORIGINS="https://log.syui.ai" ollama serve
|
||||
OLLAMA_ORIGINS="https://example.com" ollama serve
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
@@ -43,9 +72,9 @@ git push origin main
|
||||
```bash
|
||||
# 1. OAuth Client Setup (oauth/client-metadata.json)
|
||||
{
|
||||
"client_id": "https://log.syui.ai/client-metadata.json",
|
||||
"client_id": "https://example.com/client-metadata.json",
|
||||
"client_name": "ai.log Blog System",
|
||||
"redirect_uris": ["https://log.syui.ai/oauth/callback"],
|
||||
"redirect_uris": ["https://example.com/oauth/callback"],
|
||||
"scope": "atproto",
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"response_types": ["code"],
|
||||
@@ -82,8 +111,8 @@ git push origin main
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `./run.zsh c` | Enable Cloudflare tunnel (log.syui.ai) for OAuth |
|
||||
| `./run.zsh o` | Start OAuth web server (port:4173 = log.syui.ai) |
|
||||
| `./run.zsh c` | Enable Cloudflare tunnel (example.com) for OAuth |
|
||||
| `./run.zsh o` | Start OAuth web server (port:4173 = example.com) |
|
||||
| `./run.zsh co` | Start comment system (ATProto stream monitor) |
|
||||
|
||||
## 🏗️ Architecture (Pure Rust + HTML + JS)
|
||||
@@ -148,7 +177,7 @@ brew install ollama
|
||||
ollama pull gemma2:2b
|
||||
|
||||
# 2. CORS設定で起動
|
||||
OLLAMA_ORIGINS="https://log.syui.ai" ollama serve
|
||||
OLLAMA_ORIGINS="https://example.com" ollama serve
|
||||
|
||||
# 3. AI DID設定 (my-blog/templates/base.html)
|
||||
const aiConfig = {
|
||||
@@ -603,6 +632,47 @@ translation = true
|
||||
- Check AI provider configuration
|
||||
- Ensure sufficient context in memory system
|
||||
|
||||
## systemd
|
||||
|
||||
```sh
|
||||
$ sudo vim /usr/lib/systemd/system/ollama.service
|
||||
[Service]
|
||||
Environment="OLLAMA_ORIGINS=https://example.com"
|
||||
```
|
||||
|
||||
```sh
|
||||
# ファイルをsystemdディレクトリにコピー
|
||||
sudo cp ./systemd/system/ailog-stream.service /etc/systemd/system/
|
||||
sudo cp ./systemd/system/cloudflared-log.service /etc/systemd/system/
|
||||
|
||||
# 権限設定
|
||||
sudo chmod 644 /etc/systemd/system/ailog-stream.service
|
||||
sudo chmod 644 /etc/systemd/system/cloudflared-log.service
|
||||
|
||||
# systemd設定reload
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# サービス有効化・開始
|
||||
sudo systemctl enable ailog-stream.service
|
||||
sudo systemctl enable cloudflared-log.service
|
||||
|
||||
sudo systemctl start ailog-stream.service
|
||||
sudo systemctl start cloudflared-log.service
|
||||
|
||||
# 状態確認
|
||||
sudo systemctl status ailog-stream.service
|
||||
sudo systemctl status cloudflared-log.service
|
||||
|
||||
# ログ確認
|
||||
journalctl -u ailog-stream.service -f
|
||||
journalctl -u cloudflared-log.service -f
|
||||
|
||||
設定のポイント:
|
||||
- User=syui でユーザー権限で実行
|
||||
- Restart=always で異常終了時自動再起動
|
||||
- After=network.target でネットワーク起動後に実行
|
||||
- StandardOutput=journal でログをjournalctlで確認可能
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
148
action.yml
Normal file
148
action.yml
Normal file
@@ -0,0 +1,148 @@
|
||||
name: 'ailog Static Site Generator'
|
||||
description: 'AI-powered static blog generator with atproto integration'
|
||||
author: 'syui'
|
||||
|
||||
branding:
|
||||
icon: 'book-open'
|
||||
color: 'orange'
|
||||
|
||||
inputs:
|
||||
content-dir:
|
||||
description: 'Content directory containing markdown files'
|
||||
required: false
|
||||
default: 'content'
|
||||
output-dir:
|
||||
description: 'Output directory for generated site'
|
||||
required: false
|
||||
default: 'public'
|
||||
template-dir:
|
||||
description: 'Template directory'
|
||||
required: false
|
||||
default: 'templates'
|
||||
static-dir:
|
||||
description: 'Static assets directory'
|
||||
required: false
|
||||
default: 'static'
|
||||
config-file:
|
||||
description: 'Configuration file path'
|
||||
required: false
|
||||
default: 'ailog.toml'
|
||||
ai-integration:
|
||||
description: 'Enable AI features'
|
||||
required: false
|
||||
default: 'true'
|
||||
atproto-integration:
|
||||
description: 'Enable atproto/OAuth features'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
site-url:
|
||||
description: 'Generated site URL'
|
||||
value: ${{ steps.generate.outputs.site-url }}
|
||||
build-time:
|
||||
description: 'Build time in seconds'
|
||||
value: ${{ steps.generate.outputs.build-time }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- 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
|
||||
shell: bash
|
||||
run: |
|
||||
# Get latest release version (for Gitea, adjust API endpoint if needed)
|
||||
if command -v curl >/dev/null 2>&1; 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")
|
||||
else
|
||||
LATEST_VERSION="v0.1.1" # fallback version
|
||||
fi
|
||||
echo "Target 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}' 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
|
||||
echo "Download failed, building from source..."
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
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
|
||||
else
|
||||
echo "Binary is up to date"
|
||||
chmod +x ./bin/ailog
|
||||
fi
|
||||
|
||||
- name: Setup Node.js for OAuth app
|
||||
if: ${{ inputs.atproto-integration == 'true' }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Build OAuth app
|
||||
if: ${{ inputs.atproto-integration == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -d "oauth" ]; then
|
||||
cd oauth
|
||||
npm install
|
||||
npm run build
|
||||
cp -r dist/* ../${{ inputs.static-dir }}/
|
||||
fi
|
||||
|
||||
- name: Generate site
|
||||
id: generate
|
||||
shell: bash
|
||||
run: |
|
||||
start_time=$(date +%s)
|
||||
|
||||
./bin/ailog build \
|
||||
--content ${{ inputs.content-dir }} \
|
||||
--output ${{ inputs.output-dir }} \
|
||||
--templates ${{ inputs.template-dir }} \
|
||||
--static ${{ inputs.static-dir }} \
|
||||
--config ${{ inputs.config-file }}
|
||||
|
||||
end_time=$(date +%s)
|
||||
build_time=$((end_time - start_time))
|
||||
|
||||
echo "build-time=${build_time}" >> $GITHUB_OUTPUT
|
||||
echo "site-url=file://$(pwd)/${{ inputs.output-dir }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Display build summary
|
||||
shell: bash
|
||||
run: |
|
||||
echo "✅ ailog build completed successfully"
|
||||
echo "📁 Output directory: ${{ inputs.output-dir }}"
|
||||
echo "⏱️ Build time: ${{ steps.generate.outputs.build-time }}s"
|
||||
if [ -d "${{ inputs.output-dir }}" ]; then
|
||||
echo "📄 Generated files:"
|
||||
find ${{ inputs.output-dir }} -type f | head -10
|
||||
fi
|
@@ -7,6 +7,7 @@ author = "syui"
|
||||
|
||||
[build]
|
||||
highlight_code = true
|
||||
highlight_theme = "Monokai"
|
||||
minify = false
|
||||
|
||||
[ai]
|
||||
@@ -27,3 +28,4 @@ admin = "did:plc:uqzpqmrjnptsxezjx4xuh2mn"
|
||||
collection_comment = "ai.syui.log"
|
||||
collection_user = "ai.syui.log.user"
|
||||
collection_chat = "ai.syui.log.chat"
|
||||
bsky_api = "https://public.api.bsky.app"
|
||||
|
@@ -1,18 +1,14 @@
|
||||
---
|
||||
title: "静的サイトジェネレータを作った"
|
||||
slug: "ailog-system-introduction"
|
||||
slug: "ailog"
|
||||
date: "2025-06-12"
|
||||
tags: ["blog", "rust", "mcp", "atp"]
|
||||
language: ["ja", "en"]
|
||||
---
|
||||
|
||||
rustで静的サイトジェネレータを作ることにしました。[ailog](https://git.syui.ai/ai/log)といいます。`hugo`からの移行になります。
|
||||
rustで静的サイトジェネレータを作りました。。[ailog](https://git.syui.ai/ai/log)といいます。`hugo`からの移行になります。
|
||||
|
||||
ブログを書く環境もこれから変わってくると思っていて、例えば、`docs`, `readme`, `blog`などはAIが生成、または支援することになるだろうと予測しています。langの自動生成もAIが担当することになるでしょう。
|
||||
|
||||
これは、音声に限らず、プログラミング言語から、osなど、様々なtranslateがAIの自動生成になるかもしれません。
|
||||
|
||||
`ailog`は、最初にatproto-comment-system(oauth)とask-AIというAI機能をつけました。
|
||||
`ailog`は、最初にatproto-comment-system(oauth)とask-AIという機能をつけました。
|
||||
|
||||
## quick start
|
||||
|
||||
@@ -81,7 +77,13 @@ AILOG_COLLECTION_USER=ai.syui.log.user
|
||||
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
|
||||
```
|
||||
|
||||
### 解説
|
||||
これは`ailog oauth build my-blog`で`./my-blog/config.toml`から`./oauth/.env.production`が生成されます。
|
||||
|
||||
```sh
|
||||
$ ailog oauth build my-blog
|
||||
```
|
||||
|
||||
### use
|
||||
|
||||
簡単に説明すると、`./oauth`で生成するのが`atproto-comment-system`です。
|
||||
|
||||
@@ -135,3 +137,23 @@ $ ailog stream server
|
||||
|
||||
local llm, mcp, atprotoと組み合わせです。
|
||||
|
||||
|
||||
## code syntax
|
||||
|
||||
```zsh:/path/to/test.zsh
|
||||
# comment
|
||||
d=${0:a:h}
|
||||
```
|
||||
|
||||
```rust:/path/to/test.rs
|
||||
// This is a comment
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
```js:/path/to/test.js
|
||||
// This is a comment
|
||||
console.log("Hello, world!");
|
||||
```
|
||||
|
||||
|
62
my-blog/content/posts/2025-06-14-blog.md
Normal file
62
my-blog/content/posts/2025-06-14-blog.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "ブログを移行した"
|
||||
slug: "blog"
|
||||
date: 2025-06-14
|
||||
tags: ["blog", "cloudflare", "github"]
|
||||
draft: false
|
||||
---
|
||||
|
||||
ブログを移行しました。
|
||||
|
||||
1. `gh-pages`から`cf-pages`への移行になります。
|
||||
2. `hugo`からの移行で、自作の`ailog`でbuildしています。
|
||||
3. 特徴としては、`atproto`, `AI`との連携です。
|
||||
|
||||
```yml:.github/workflows/cloudflare-pages.yml
|
||||
name: Deploy to Cloudflare Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build ailog
|
||||
run: cargo build --release
|
||||
|
||||
- name: Build site with ailog
|
||||
run: |
|
||||
cd my-blog
|
||||
../target/release/ailog build
|
||||
|
||||
- name: List public directory
|
||||
run: |
|
||||
ls -la my-blog/public/
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: my-blog/public
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
wranglerVersion: '3'
|
||||
```
|
||||
|
@@ -1,34 +0,0 @@
|
||||
---
|
||||
title: "Welcome to ailog"
|
||||
slug: welcome-to-ailog
|
||||
date: 2025-01-06
|
||||
tags: ["welcome", "ailog"]
|
||||
language: en
|
||||
---
|
||||
|
||||
# Welcome to ailog
|
||||
|
||||
This is your first post powered by **ailog** - a static blog generator with AI features.
|
||||
|
||||
## Features
|
||||
|
||||
- Fast static site generation
|
||||
- Markdown support with frontmatter
|
||||
- AI-powered features (coming soon)
|
||||
- atproto integration for comments
|
||||
|
||||
## Getting Started
|
||||
|
||||
Create new posts with:
|
||||
|
||||
```bash
|
||||
ailog new "My New Post"
|
||||
```
|
||||
|
||||
Build your blog with:
|
||||
|
||||
```bash
|
||||
ailog build
|
||||
```
|
||||
|
||||
Happy blogging!
|
@@ -16,11 +16,32 @@
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/css/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
Content-Type: text/css
|
||||
Cache-Control: public, max-age=60
|
||||
|
||||
/*.js
|
||||
Content-Type: application/javascript
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/assets/*.js
|
||||
Content-Type: application/javascript
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# Ensure ES6 modules are served correctly
|
||||
/assets/comment-atproto-*.js
|
||||
Content-Type: text/javascript; charset=utf-8
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# All JS assets
|
||||
/assets/*-*.js
|
||||
Content-Type: text/javascript; charset=utf-8
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# CSS assets
|
||||
/assets/*.css
|
||||
Content-Type: text/css
|
||||
Cache-Control: public, max-age=60
|
||||
|
||||
/posts/*
|
||||
Cache-Control: public, max-age=3600
|
||||
|
||||
|
BIN
my-blog/static/apple-touch-icon.png
Normal file
BIN
my-blog/static/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"client_id": "https://log.syui.ai/client-metadata.json",
|
||||
"client_id": "https://syui.ai/client-metadata.json",
|
||||
"client_name": "ai.card",
|
||||
"client_uri": "https://log.syui.ai",
|
||||
"logo_uri": "https://log.syui.ai/favicon.ico",
|
||||
"tos_uri": "https://log.syui.ai/terms",
|
||||
"policy_uri": "https://log.syui.ai/privacy",
|
||||
"client_uri": "https://syui.ai",
|
||||
"logo_uri": "https://syui.ai/favicon.ico",
|
||||
"tos_uri": "https://syui.ai/terms",
|
||||
"policy_uri": "https://syui.ai/privacy",
|
||||
"redirect_uris": [
|
||||
"https://log.syui.ai/oauth/callback",
|
||||
"https://log.syui.ai/"
|
||||
"https://syui.ai/oauth/callback",
|
||||
"https://syui.ai/"
|
||||
],
|
||||
"response_types": [
|
||||
"code"
|
||||
|
File diff suppressed because it is too large
Load Diff
342
my-blog/static/css/svg-animation-package.css
Normal file
342
my-blog/static/css/svg-animation-package.css
Normal file
@@ -0,0 +1,342 @@
|
||||
/* SVG Animation Package - Dependency-free standalone package
|
||||
* Based on svg-animation-particle-circle.css
|
||||
* Theme color integration with CSS variables
|
||||
*/
|
||||
|
||||
/* Theme-based color variables for particles */
|
||||
:root {
|
||||
--particle-color-1: #f40; /* theme-color base */
|
||||
--particle-color-2: #f50; /* theme-color +0.1 brightness */
|
||||
--particle-color-3: #f60; /* theme-color +0.2 brightness */
|
||||
--particle-color-4: #f70; /* theme-color +0.3 brightness */
|
||||
--particle-color-5: #f80; /* theme-color +0.4 brightness */
|
||||
--explosion-color: #f30; /* theme-color -0.1 brightness */
|
||||
--syui-color: #f40; /* main theme color */
|
||||
}
|
||||
|
||||
/* Core SVG button setup */
|
||||
.likeButton {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Remove debug animation and restore hover functionality */
|
||||
|
||||
.likeButton .border {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
/* Explosion circle - initially hidden */
|
||||
.likeButton .explosion {
|
||||
transform-origin: center center;
|
||||
transform: scale(1);
|
||||
stroke: var(--explosion-color);
|
||||
fill: none;
|
||||
opacity: 0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
/* Particle layer - initially hidden */
|
||||
.likeButton .particleLayer {
|
||||
opacity: 0;
|
||||
transform: scale(0); /* Ensure particles start hidden */
|
||||
}
|
||||
|
||||
.likeButton .particleLayer circle {
|
||||
opacity: 0;
|
||||
transform-origin: center center; /* Fixed from 250px 250px */
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
/* Syui logo - main animation target */
|
||||
.likeButton .syui {
|
||||
fill: var(--syui-color);
|
||||
transform: scale(1);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* Hover trigger - replaces jQuery */
|
||||
.likeButton:hover .explosion {
|
||||
animation: explosionAnime 800ms forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer {
|
||||
animation: particleLayerAnime 800ms forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .syui,
|
||||
.likeButton:hover path.syui {
|
||||
animation: syuiDeluxeAnime 400ms forwards;
|
||||
}
|
||||
|
||||
/* Individual particle animations */
|
||||
.likeButton:hover .particleLayer circle:nth-child(1) {
|
||||
animation: particleAnimate1 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(2) {
|
||||
animation: particleAnimate2 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(3) {
|
||||
animation: particleAnimate3 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(4) {
|
||||
animation: particleAnimate4 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(5) {
|
||||
animation: particleAnimate5 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(6) {
|
||||
animation: particleAnimate6 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(7) {
|
||||
animation: particleAnimate7 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(8) {
|
||||
animation: particleAnimate8 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(9) {
|
||||
animation: particleAnimate9 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(10) {
|
||||
animation: particleAnimate10 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(11) {
|
||||
animation: particleAnimate11 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(12) {
|
||||
animation: particleAnimate12 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(13) {
|
||||
animation: particleAnimate13 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer circle:nth-child(14) {
|
||||
animation: particleAnimate14 800ms;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
/* Keyframe animations */
|
||||
@keyframes explosionAnime {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.01);
|
||||
}
|
||||
1% {
|
||||
opacity: 1;
|
||||
transform: scale(0.01);
|
||||
}
|
||||
5% {
|
||||
stroke-width: 200;
|
||||
}
|
||||
20% {
|
||||
stroke-width: 300;
|
||||
}
|
||||
50% {
|
||||
stroke: var(--particle-color-3);
|
||||
transform: scale(1.1);
|
||||
stroke-width: 1;
|
||||
}
|
||||
50.1% {
|
||||
stroke-width: 0;
|
||||
}
|
||||
100% {
|
||||
stroke: var(--particle-color-3);
|
||||
transform: scale(1.1);
|
||||
stroke-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes particleLayerAnime {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
}
|
||||
31% {
|
||||
opacity: 1;
|
||||
}
|
||||
60% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(0, -20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Syui Deluxe Animation - Based on 2019 blog post */
|
||||
@keyframes syuiDeluxeAnime {
|
||||
0% {
|
||||
fill: var(--syui-color);
|
||||
transform: scale(1) translate(0%, 0%);
|
||||
}
|
||||
40% {
|
||||
fill: color-mix(in srgb, var(--syui-color) 40%, transparent);
|
||||
transform: scale(1, 0.9) translate(-9%, 9%);
|
||||
}
|
||||
50% {
|
||||
fill: color-mix(in srgb, var(--syui-color) 70%, transparent);
|
||||
transform: scale(1, 0.9) translate(-7%, 7%);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1) translate(-7%, 7%);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.04) translate(-5%, 5%);
|
||||
}
|
||||
80% {
|
||||
fill: color-mix(in srgb, var(--syui-color) 60%, transparent);
|
||||
transform: scale(1.04) translate(-5%, 5%);
|
||||
}
|
||||
90% {
|
||||
fill: var(--particle-color-5); /* 爆発の閃光 */
|
||||
transform: scale(1) translate(0%);
|
||||
}
|
||||
100% {
|
||||
fill: var(--syui-color);
|
||||
transform: scale(1, 1) translate(0%, 0%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Individual particle animations */
|
||||
@keyframes particleAnimate1 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-16px, -59px); }
|
||||
90% { transform: translate(-16px, -59px); }
|
||||
100% { opacity: 1; transform: translate(-16px, -59px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate2 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(41px, 43px); }
|
||||
90% { transform: translate(41px, 43px); }
|
||||
100% { opacity: 1; transform: translate(41px, 43px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate3 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(50px, -48px); }
|
||||
90% { transform: translate(50px, -48px); }
|
||||
100% { opacity: 1; transform: translate(50px, -48px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate4 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-39px, 36px); }
|
||||
90% { transform: translate(-39px, 36px); }
|
||||
100% { opacity: 1; transform: translate(-39px, 36px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate5 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-39px, 32px); }
|
||||
90% { transform: translate(-39px, 32px); }
|
||||
100% { opacity: 1; transform: translate(-39px, 32px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate6 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(48px, 6px); }
|
||||
90% { transform: translate(48px, 6px); }
|
||||
100% { opacity: 1; transform: translate(48px, 6px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate7 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-69px, -36px); }
|
||||
90% { transform: translate(-69px, -36px); }
|
||||
100% { opacity: 1; transform: translate(-69px, -36px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate8 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-12px, -52px); }
|
||||
90% { transform: translate(-12px, -52px); }
|
||||
100% { opacity: 1; transform: translate(-12px, -52px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate9 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-43px, -21px); }
|
||||
90% { transform: translate(-43px, -21px); }
|
||||
100% { opacity: 1; transform: translate(-43px, -21px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate10 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-10px, 47px); }
|
||||
90% { transform: translate(-10px, 47px); }
|
||||
100% { opacity: 1; transform: translate(-10px, 47px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate11 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(66px, -9px); }
|
||||
90% { transform: translate(66px, -9px); }
|
||||
100% { opacity: 1; transform: translate(66px, -9px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate12 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(40px, -45px); }
|
||||
90% { transform: translate(40px, -45px); }
|
||||
100% { opacity: 1; transform: translate(40px, -45px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate13 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(29px, 24px); }
|
||||
90% { transform: translate(29px, 24px); }
|
||||
100% { opacity: 1; transform: translate(29px, 24px); }
|
||||
}
|
||||
|
||||
@keyframes particleAnimate14 {
|
||||
0% { transform: translate(0, 0); }
|
||||
30% { opacity: 1; transform: translate(0, 0); }
|
||||
80% { transform: translate(-10px, 50px); }
|
||||
90% { transform: translate(-10px, 50px); }
|
||||
100% { opacity: 1; transform: translate(-10px, 50px); }
|
||||
}
|
BIN
my-blog/static/favicon.ico
Normal file
BIN
my-blog/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
BIN
my-blog/static/favicon.png
Normal file
BIN
my-blog/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
22
my-blog/static/favicon.svg
Normal file
22
my-blog/static/favicon.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="77pt" height="77pt" viewBox="0 0 512 512" class="likeButton" >
|
||||
<circle class="explosion" r="150" cx="250" cy="250"></circle>
|
||||
<g class="particleLayer">
|
||||
<circle fill="#ef454aba" cx="130" cy="126.5" r="12.5"/>
|
||||
<circle fill="#ef454acc" cx="411" cy="313.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="279" cy="86.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="155" cy="390.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="89" cy="292.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="414" cy="282.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="115" cy="149.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="250" cy="80.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="78" cy="261.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="182" cy="402.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="401.5" cy="166" r="13"/>
|
||||
<circle fill="#ef454aba" cx="379" cy="141.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="327" cy="397.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="296" cy="392.5" r="10.5"/>
|
||||
</g>
|
||||
<g transform="translate(0,512) scale(0.1,-0.1)" fill="#000000" class="icon_syui">
|
||||
<path class="syui" d="M3660 4460 c-11 -11 -33 -47 -48 -80 l-29 -60 -12 38 c-27 88 -58 92 -98 11 -35 -70 -73 -159 -73 -169 0 -6 -5 -10 -10 -10 -6 0 -15 -10 -21 -22 -33 -73 -52 -92 -47 -48 2 26 -1 35 -14 38 -16 3 -168 -121 -168 -138 0 -5 -13 -16 -28 -24 -24 -13 -35 -12 -87 0 -221 55 -231 56 -480 56 -219 1 -247 -1 -320 -22 -44 -12 -96 -26 -115 -30 -57 -13 -122 -39 -200 -82 -8 -4 -31 -14 -50 -23 -41 -17 -34 -13 -146 -90 -87 -59 -292 -252 -351 -330 -63 -83 -143 -209 -143 -225 0 -10 -7 -23 -15 -30 -8 -7 -15 -17 -15 -22 0 -5 -13 -37 -28 -71 -16 -34 -36 -93 -45 -132 -9 -38 -24 -104 -34 -145 -13 -60 -17 -121 -17 -300 1 -224 1 -225 36 -365 24 -94 53 -175 87 -247 28 -58 51 -108 51 -112 0 -3 13 -24 28 -48 42 -63 46 -79 22 -85 -11 -3 -20 -9 -20 -14 0 -5 -4 -9 -10 -9 -5 0 -22 -11 -37 -25 -16 -13 -75 -59 -133 -100 -58 -42 -113 -82 -123 -90 -9 -8 -22 -15 -27 -15 -6 0 -10 -6 -10 -13 0 -8 -11 -20 -25 -27 -34 -18 -34 -54 0 -48 14 3 25 2 25 -1 0 -3 -43 -31 -95 -61 -52 -30 -95 -58 -95 -62 0 -5 -5 -8 -11 -8 -19 0 -84 -33 -92 -47 -4 -7 -15 -13 -22 -13 -14 0 -17 -4 -19 -32 -1 -8 15 -15 37 -18 l38 -5 -47 -48 c-56 -59 -54 -81 9 -75 30 3 45 0 54 -11 9 -13 16 -14 43 -4 29 11 30 10 18 -5 -7 -9 -19 -23 -25 -30 -7 -7 -13 -20 -13 -29 0 -12 8 -14 38 -9 20 4 57 8 82 9 25 2 54 8 66 15 18 10 23 8 32 -13 17 -38 86 -35 152 6 27 17 50 34 50 38 0 16 62 30 85 19 33 -15 72 -2 89 30 8 15 31 43 51 62 35 34 38 35 118 35 77 0 85 2 126 33 24 17 52 32 61 32 9 0 42 18 73 40 30 22 61 40 69 40 21 0 88 -26 100 -38 7 -7 17 -12 24 -12 7 0 35 -11 62 -25 66 -33 263 -84 387 -101 189 -25 372 -12 574 41 106 27 130 37 261 97 41 20 80 37 85 39 6 2 51 31 100 64 166 111 405 372 489 534 10 20 22 43 27 51 5 8 12 22 15 30 3 8 17 40 31 70 54 115 95 313 108 520 13 200 -43 480 -134 672 -28 58 -51 108 -51 112 0 3 -13 24 -29 48 -15 24 -34 60 -40 80 -19 57 3 142 50 193 10 11 22 49 28 85 6 36 16 67 21 68 18 6 31 53 25 83 -4 18 -17 33 -36 41 -16 7 -29 15 -29 18 1 10 38 50 47 50 5 0 20 11 33 25 18 19 22 31 17 61 -3 20 -14 45 -23 55 -16 18 -16 20 6 44 15 16 21 32 18 49 -3 15 1 34 8 43 32 43 7 73 -46 55 l-30 -11 0 85 c0 74 -2 84 -18 84 -21 0 -53 -33 -103 -104 l-34 -48 -5 74 c-7 102 -35 133 -80 88z m-870 -740 c36 -7 75 -14 88 -16 21 -4 23 -9 16 -37 -3 -18 -14 -43 -24 -57 -10 -14 -20 -35 -24 -46 -4 -12 -16 -32 -27 -45 -12 -13 -37 -49 -56 -79 -20 -30 -52 -73 -72 -96 -53 -60 -114 -133 -156 -189 -21 -27 -44 -54 -52 -58 -7 -4 -13 -14 -13 -22 0 -7 -18 -33 -40 -57 -22 -23 -40 -46 -40 -50 0 -5 -19 -21 -42 -38 -47 -35 -85 -38 -188 -15 -115 25 -173 20 -264 -23 -45 -22 -106 -46 -136 -56 -48 -15 -77 -25 -140 -50 -70 -28 -100 -77 -51 -84 14 -2 34 -10 45 -17 12 -7 53 -16 91 -20 90 -9 131 -22 178 -57 20 -16 52 -35 70 -43 18 -7 40 -22 49 -32 16 -18 15 -22 -24 -88 -23 -39 -47 -74 -53 -80 -7 -5 -23 -26 -36 -45 -26 -39 -92 -113 -207 -232 -4 -4 -37 -36 -73 -71 l-66 -64 -20 41 c-58 119 -105 240 -115 301 -40 244 -35 409 20 595 8 30 21 66 28 80 7 14 24 54 38 89 15 35 35 75 46 89 11 13 20 31 20 38 0 8 3 14 8 14 4 0 16 16 27 36 24 45 221 245 278 281 23 15 44 30 47 33 20 20 138 78 250 123 61 24 167 50 250 61 60 7 302 -1 370 -14z m837 -661 c52 -101 102 -279 106 -379 2 -42 0 -45 -28 -51 -16 -4 -101 -7 -187 -8 -166 -1 -229 10 -271 49 -19 19 -19 19 14 49 22 21 44 31 65 31 41 0 84 34 84 66 0 30 12 55 56 112 19 25 37 65 44 95 11 51 53 111 74 104 6 -2 25 -32 43 -68z m-662 -810 c17 -10 40 -24 53 -30 12 -7 22 -16 22 -20 0 -4 17 -13 38 -19 20 -7 44 -18 52 -24 8 -7 33 -21 55 -31 22 -11 42 -23 45 -26 11 -14 109 -49 164 -58 62 -11 101 -7 126 14 15 14 38 18 78 16 39 -2 26 -41 -49 -146 -78 -109 -85 -118 -186 -219 -61 -61 -239 -189 -281 -203 -17 -5 -73 -29 -104 -44 -187 -92 -605 -103 -791 -21 -42 19 -47 24 -37 41 5 11 28 32 51 48 22 15 51 38 64 51 13 12 28 22 33 22 17 0 242 233 242 250 0 6 5 10 10 10 6 0 10 6 10 14 0 25 50 55 100 62 59 8 56 6 115 83 50 66 74 117 75 162 0 14 7 40 16 57 18 38 52 41 99 11z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
@@ -1,3 +1,3 @@
|
||||
<!-- OAuth Comment System - Load globally for session management -->
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-CDastf61.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-B330B6QX.css">
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-Do1JWJCw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-CPKYAM8U.css">
|
360
my-blog/static/js/ask-ai.js
Normal file
360
my-blog/static/js/ask-ai.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Ask AI functionality - Pure JavaScript, no jQuery dependency
|
||||
*/
|
||||
class AskAI {
|
||||
constructor() {
|
||||
this.isReady = false;
|
||||
this.aiProfile = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.checkAuthOnLoad();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Listen for AI ready signal
|
||||
window.addEventListener('aiChatReady', () => {
|
||||
this.isReady = true;
|
||||
console.log('AI Chat is ready');
|
||||
});
|
||||
|
||||
// Listen for AI profile updates
|
||||
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');
|
||||
|
||||
if (isAuthenticated) {
|
||||
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() {
|
||||
setTimeout(() => {
|
||||
this.checkAuth();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
observeAuth() {
|
||||
const observer = new MutationObserver(() => {
|
||||
const userSections = document.querySelectorAll('.user-section');
|
||||
if (userSections.length > 0) {
|
||||
this.checkAuth();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
updateButton() {
|
||||
const button = document.getElementById('askAiButton');
|
||||
if (this.aiProfile && this.aiProfile.displayName) {
|
||||
const textNode = button.childNodes[2];
|
||||
if (textNode) {
|
||||
textNode.textContent = this.aiProfile.displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showGreeting() {
|
||||
if (!this.aiProfile) return;
|
||||
|
||||
const chatHistory = document.getElementById('chatHistory');
|
||||
const greetingDiv = document.createElement('div');
|
||||
greetingDiv.className = 'chat-message ai-message comment-style initial-greeting';
|
||||
|
||||
const avatarElement = this.aiProfile.avatar
|
||||
? `<img src="${this.aiProfile.avatar}" alt="${this.aiProfile.displayName}" class="profile-avatar">`
|
||||
: '🤖';
|
||||
|
||||
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 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 ask() {
|
||||
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 (!this.isReady) {
|
||||
await this.waitForReady();
|
||||
}
|
||||
|
||||
// Disable button
|
||||
askButton.disabled = true;
|
||||
askButton.textContent = 'Posting...';
|
||||
|
||||
try {
|
||||
// Add user message
|
||||
this.addUserMessage(question);
|
||||
|
||||
// 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 class="message-content">${question}</div>
|
||||
`;
|
||||
chatHistory.appendChild(questionDiv);
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
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);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const chatHistory = document.getElementById('chatHistory');
|
||||
this.removeLoading();
|
||||
|
||||
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">${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
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
window.askAIInstance = new AskAI();
|
||||
console.log('Ask AI initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Ask AI:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Global function for onclick
|
||||
window.AskAI = {
|
||||
toggle: function() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
94
my-blog/static/js/theme.js
Normal file
94
my-blog/static/js/theme.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Theme and visual effects - Pure CSS animations, no jQuery
|
||||
*/
|
||||
class Theme {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupParticleColors();
|
||||
this.setupLogoAnimations();
|
||||
}
|
||||
|
||||
setupParticleColors() {
|
||||
// Dynamic particle colors based on theme
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
/* Dynamic particle colors based on theme */
|
||||
.likeButton .particleLayer circle:nth-child(1),
|
||||
.likeButton .particleLayer circle:nth-child(2) {
|
||||
fill: var(--particle-color-1) !important;
|
||||
}
|
||||
|
||||
.likeButton .particleLayer circle:nth-child(3),
|
||||
.likeButton .particleLayer circle:nth-child(4) {
|
||||
fill: var(--particle-color-2) !important;
|
||||
}
|
||||
|
||||
.likeButton .particleLayer circle:nth-child(5),
|
||||
.likeButton .particleLayer circle:nth-child(6),
|
||||
.likeButton .particleLayer circle:nth-child(7) {
|
||||
fill: var(--particle-color-3) !important;
|
||||
}
|
||||
|
||||
.likeButton .particleLayer circle:nth-child(8),
|
||||
.likeButton .particleLayer circle:nth-child(9),
|
||||
.likeButton .particleLayer circle:nth-child(10) {
|
||||
fill: var(--particle-color-4) !important;
|
||||
}
|
||||
|
||||
.likeButton .particleLayer circle:nth-child(11),
|
||||
.likeButton .particleLayer circle:nth-child(12),
|
||||
.likeButton .particleLayer circle:nth-child(13),
|
||||
.likeButton .particleLayer circle:nth-child(14) {
|
||||
fill: var(--particle-color-5) !important;
|
||||
}
|
||||
|
||||
/* Reset initial animations but allow hover */
|
||||
.likeButton .syui {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.likeButton .particleLayer {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.likeButton .explosion {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Enable hover animations from package */
|
||||
.likeButton:hover .syui,
|
||||
.likeButton:hover path.syui {
|
||||
animation: syuiDeluxeAnime 400ms forwards !important;
|
||||
}
|
||||
|
||||
.likeButton:hover .particleLayer {
|
||||
animation: particleLayerAnime 800ms forwards !important;
|
||||
}
|
||||
|
||||
.likeButton:hover .explosion {
|
||||
animation: explosionAnime 800ms forwards !important;
|
||||
}
|
||||
|
||||
/* Logo positioning */
|
||||
.logo .likeButton {
|
||||
background: transparent !important;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupLogoAnimations() {
|
||||
// Pure CSS animations are handled by the svg-animation-package.css
|
||||
// This method is reserved for any future JavaScript-based enhancements
|
||||
console.log('Logo animations initialized (CSS-based)');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new Theme();
|
||||
});
|
165
my-blog/static/pkg/font-awesome/LICENSE.txt
Normal file
165
my-blog/static/pkg/font-awesome/LICENSE.txt
Normal file
@@ -0,0 +1,165 @@
|
||||
Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Font Awesome Free License
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Fonts: SIL OFL 1.1 License
|
||||
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
|
||||
with Reserved Font Name: "Font Awesome".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE
|
||||
Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting — in part or in whole — any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
Copyright 2024 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Attribution
|
||||
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Brand Icons
|
||||
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
9
my-blog/static/pkg/font-awesome/css/all.min.css
vendored
Normal file
9
my-blog/static/pkg/font-awesome/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
my-blog/static/pkg/font-awesome/css/brands.min.css
vendored
Normal file
6
my-blog/static/pkg/font-awesome/css/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
my-blog/static/pkg/font-awesome/css/fontawesome.min.css
vendored
Normal file
9
my-blog/static/pkg/font-awesome/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
my-blog/static/pkg/font-awesome/css/regular.min.css
vendored
Normal file
6
my-blog/static/pkg/font-awesome/css/regular.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
|
6
my-blog/static/pkg/font-awesome/css/solid.min.css
vendored
Normal file
6
my-blog/static/pkg/font-awesome/css/solid.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-brands-400.ttf
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-brands-400.woff2
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-regular-400.ttf
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-regular-400.woff2
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-solid-900.ttf
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-solid-900.woff2
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-v4compatibility.ttf
Normal file
BIN
my-blog/static/pkg/font-awesome/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.eot
Normal file
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.eot
Normal file
Binary file not shown.
34
my-blog/static/pkg/icomoon/fonts/icomoon.svg
Normal file
34
my-blog/static/pkg/icomoon/fonts/icomoon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 58 KiB |
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.ttf
Normal file
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.ttf
Normal file
Binary file not shown.
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.woff
Normal file
BIN
my-blog/static/pkg/icomoon/fonts/icomoon.woff
Normal file
Binary file not shown.
99
my-blog/static/pkg/icomoon/style.css
Normal file
99
my-blog/static/pkg/icomoon/style.css
Normal file
@@ -0,0 +1,99 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('fonts/icomoon.eot?mxezzh');
|
||||
src: url('fonts/icomoon.eot?mxezzh#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?mxezzh') format('truetype'),
|
||||
url('fonts/icomoon.woff?mxezzh') format('woff'),
|
||||
url('fonts/icomoon.svg?mxezzh#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-git:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-cube:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-game:before {
|
||||
content: "\e9d5";
|
||||
}
|
||||
.icon-card:before {
|
||||
content: "\e9d6";
|
||||
}
|
||||
.icon-book:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
.icon-git1:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
.icon-moji_a:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
.icon-archlinux:before {
|
||||
content: "\e9c4";
|
||||
}
|
||||
.icon-archlinuxjp:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
.icon-syui:before {
|
||||
content: "\e9c6";
|
||||
}
|
||||
.icon-phoenix-power:before {
|
||||
content: "\e9c7";
|
||||
}
|
||||
.icon-phoenix-world:before {
|
||||
content: "\e9c8";
|
||||
}
|
||||
.icon-power:before {
|
||||
content: "\e9c9";
|
||||
}
|
||||
.icon-phoenix:before {
|
||||
content: "\e9ca";
|
||||
}
|
||||
.icon-honeycomb:before {
|
||||
content: "\e9cb";
|
||||
}
|
||||
.icon-ai:before {
|
||||
content: "\e9cc";
|
||||
}
|
||||
.icon-robot:before {
|
||||
content: "\e9cd";
|
||||
}
|
||||
.icon-sandar:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
.icon-moon:before {
|
||||
content: "\e9cf";
|
||||
}
|
||||
.icon-home:before {
|
||||
content: "\e9d0";
|
||||
}
|
||||
.icon-cloud:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
.icon-api:before {
|
||||
content: "\e9d2";
|
||||
}
|
||||
.icon-aibadge:before {
|
||||
content: "\ebf8";
|
||||
}
|
||||
.icon-aiterm:before {
|
||||
content: "\ebf7";
|
||||
}
|
24
my-blog/static/syui.svg
Normal file
24
my-blog/static/syui.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="77pt" height="77pt" viewBox="0 0 512 512" class="likeButton" >
|
||||
<circle class="explosion" r="150" cx="250" cy="250"></circle>
|
||||
<g class="particleLayer">
|
||||
<circle fill="#ef454aba" cx="130" cy="126.5" r="12.5"/>
|
||||
<circle fill="#ef454acc" cx="411" cy="313.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="279" cy="86.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="155" cy="390.5" r="12.5"/>
|
||||
<circle fill="#ef454aba" cx="89" cy="292.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="414" cy="282.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="115" cy="149.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="250" cy="80.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="78" cy="261.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="182" cy="402.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="401.5" cy="166" r="13"/>
|
||||
<circle fill="#ef454aba" cx="379" cy="141.5" r="10.5"/>
|
||||
<circle fill="#ef454a91" cx="327" cy="397.5" r="10.5"/>
|
||||
<circle fill="#ef454aba" cx="296" cy="392.5" r="10.5"/>
|
||||
</g>
|
||||
<g transform="translate(0,512) scale(0.1,-0.1)" fill="#000000" class="icon_syui">
|
||||
|
||||
<path class="syui" d="M3660 4460 c-11 -11 -33 -47 -48 -80 l-29 -60 -12 38 c-27 88 -58 92 -98 11 -35 -70 -73 -159 -73 -169 0 -6 -5 -10 -10 -10 -6 0 -15 -10 -21 -22 -33 -73 -52 -92 -47 -48 2 26 -1 35 -14 38 -16 3 -168 -121 -168 -138 0 -5 -13 -16 -28 -24 -24 -13 -35 -12 -87 0 -221 55 -231 56 -480 56 -219 1 -247 -1 -320 -22 -44 -12 -96 -26 -115 -30 -57 -13 -122 -39 -200 -82 -8 -4 -31 -14 -50 -23 -41 -17 -34 -13 -146 -90 -87 -59 -292 -252 -351 -330 -63 -83 -143 -209 -143 -225 0 -10 -7 -23 -15 -30 -8 -7 -15 -17 -15 -22 0 -5 -13 -37 -28 -71 -16 -34 -36 -93 -45 -132 -9 -38 -24 -104 -34 -145 -13 -60 -17 -121 -17 -300 1 -224 1 -225 36 -365 24 -94 53 -175 87 -247 28 -58 51 -108 51 -112 0 -3 13 -24 28 -48 42 -63 46 -79 22 -85 -11 -3 -20 -9 -20 -14 0 -5 -4 -9 -10 -9 -5 0 -22 -11 -37 -25 -16 -13 -75 -59 -133 -100 -58 -42 -113 -82 -123 -90 -9 -8 -22 -15 -27 -15 -6 0 -10 -6 -10 -13 0 -8 -11 -20 -25 -27 -34 -18 -34 -54 0 -48 14 3 25 2 25 -1 0 -3 -43 -31 -95 -61 -52 -30 -95 -58 -95 -62 0 -5 -5 -8 -11 -8 -19 0 -84 -33 -92 -47 -4 -7 -15 -13 -22 -13 -14 0 -17 -4 -19 -32 -1 -8 15 -15 37 -18 l38 -5 -47 -48 c-56 -59 -54 -81 9 -75 30 3 45 0 54 -11 9 -13 16 -14 43 -4 29 11 30 10 18 -5 -7 -9 -19 -23 -25 -30 -7 -7 -13 -20 -13 -29 0 -12 8 -14 38 -9 20 4 57 8 82 9 25 2 54 8 66 15 18 10 23 8 32 -13 17 -38 86 -35 152 6 27 17 50 34 50 38 0 16 62 30 85 19 33 -15 72 -2 89 30 8 15 31 43 51 62 35 34 38 35 118 35 77 0 85 2 126 33 24 17 52 32 61 32 9 0 42 18 73 40 30 22 61 40 69 40 21 0 88 -26 100 -38 7 -7 17 -12 24 -12 7 0 35 -11 62 -25 66 -33 263 -84 387 -101 189 -25 372 -12 574 41 106 27 130 37 261 97 41 20 80 37 85 39 6 2 51 31 100 64 166 111 405 372 489 534 10 20 22 43 27 51 5 8 12 22 15 30 3 8 17 40 31 70 54 115 95 313 108 520 13 200 -43 480 -134 672 -28 58 -51 108 -51 112 0 3 -13 24 -29 48 -15 24 -34 60 -40 80 -19 57 3 142 50 193 10 11 22 49 28 85 6 36 16 67 21 68 18 6 31 53 25 83 -4 18 -17 33 -36 41 -16 7 -29 15 -29 18 1 10 38 50 47 50 5 0 20 11 33 25 18 19 22 31 17 61 -3 20 -14 45 -23 55 -16 18 -16 20 6 44 15 16 21 32 18 49 -3 15 1 34 8 43 32 43 7 73 -46 55 l-30 -11 0 85 c0 74 -2 84 -18 84 -21 0 -53 -33 -103 -104 l-34 -48 -5 74 c-7 102 -35 133 -80 88z m-870 -740 c36 -7 75 -14 88 -16 21 -4 23 -9 16 -37 -3 -18 -14 -43 -24 -57 -10 -14 -20 -35 -24 -46 -4 -12 -16 -32 -27 -45 -12 -13 -37 -49 -56 -79 -20 -30 -52 -73 -72 -96 -53 -60 -114 -133 -156 -189 -21 -27 -44 -54 -52 -58 -7 -4 -13 -14 -13 -22 0 -7 -18 -33 -40 -57 -22 -23 -40 -46 -40 -50 0 -5 -19 -21 -42 -38 -47 -35 -85 -38 -188 -15 -115 25 -173 20 -264 -23 -45 -22 -106 -46 -136 -56 -48 -15 -77 -25 -140 -50 -70 -28 -100 -77 -51 -84 14 -2 34 -10 45 -17 12 -7 53 -16 91 -20 90 -9 131 -22 178 -57 20 -16 52 -35 70 -43 18 -7 40 -22 49 -32 16 -18 15 -22 -24 -88 -23 -39 -47 -74 -53 -80 -7 -5 -23 -26 -36 -45 -26 -39 -92 -113 -207 -232 -4 -4 -37 -36 -73 -71 l-66 -64 -20 41 c-58 119 -105 240 -115 301 -40 244 -35 409 20 595 8 30 21 66 28 80 7 14 24 54 38 89 15 35 35 75 46 89 11 13 20 31 20 38 0 8 3 14 8 14 4 0 16 16 27 36 24 45 221 245 278 281 23 15 44 30 47 33 20 20 138 78 250 123 61 24 167 50 250 61 60 7 302 -1 370 -14z m837 -661 c52 -101 102 -279 106 -379 2 -42 0 -45 -28 -51 -16 -4 -101 -7 -187 -8 -166 -1 -229 10 -271 49 -19 19 -19 19 14 49 22 21 44 31 65 31 41 0 84 34 84 66 0 30 12 55 56 112 19 25 37 65 44 95 11 51 53 111 74 104 6 -2 25 -32 43 -68z m-662 -810 c17 -10 40 -24 53 -30 12 -7 22 -16 22 -20 0 -4 17 -13 38 -19 20 -7 44 -18 52 -24 8 -7 33 -21 55 -31 22 -11 42 -23 45 -26 11 -14 109 -49 164 -58 62 -11 101 -7 126 14 15 14 38 18 78 16 39 -2 26 -41 -49 -146 -78 -109 -85 -118 -186 -219 -61 -61 -239 -189 -281 -203 -17 -5 -73 -29 -104 -44 -187 -92 -605 -103 -791 -21 -42 19 -47 24 -37 41 5 11 28 32 51 48 22 15 51 38 64 51 13 12 28 22 33 22 17 0 242 233 242 250 0 6 5 10 10 10 6 0 10 6 10 14 0 25 50 55 100 62 59 8 56 6 115 83 50 66 74 117 75 162 0 14 7 40 16 57 18 38 52 41 99 11z"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
@@ -4,8 +4,16 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ config.title }}{% endblock %}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<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">
|
||||
<link rel="stylesheet" href="/css/svg-animation-package.css">
|
||||
<link rel="stylesheet" href="/pkg/icomoon/style.css">
|
||||
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css">
|
||||
|
||||
{% include "oauth-assets.html" %}
|
||||
{% block head %}{% endblock %}
|
||||
@@ -15,31 +23,53 @@
|
||||
<header class="main-header">
|
||||
<div class="header-content">
|
||||
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<svg width="77pt" height="77pt" viewBox="0 0 512 512" class="likeButton">
|
||||
<circle class="explosion" r="150" cx="250" cy="250"></circle>
|
||||
<g class="particleLayer">
|
||||
<circle fill="#8CE8C3" cx="130" cy="126.5" r="12.5"></circle>
|
||||
<circle fill="#8CE8C3" cx="411" cy="313.5" r="12.5"></circle>
|
||||
<circle fill="#91D2FA" cx="279" cy="86.5" r="12.5"></circle>
|
||||
<circle fill="#91D2FA" cx="155" cy="390.5" r="12.5"></circle>
|
||||
<circle fill="#CC8EF5" cx="89" cy="292.5" r="10.5"></circle>
|
||||
<circle fill="#9BDFBA" cx="414" cy="282.5" r="10.5"></circle>
|
||||
<circle fill="#9BDFBA" cx="115" cy="149.5" r="10.5"></circle>
|
||||
<circle fill="#9FC7FA" cx="250" cy="80.5" r="10.5"></circle>
|
||||
<circle fill="#9FC7FA" cx="78" cy="261.5" r="10.5"></circle>
|
||||
<circle fill="#96D8E9" cx="182" cy="402.5" r="10.5"></circle>
|
||||
<circle fill="#CC8EF5" cx="401.5" cy="166" r="13"></circle>
|
||||
<circle fill="#DB92D0" cx="379" cy="141.5" r="10.5"></circle>
|
||||
<circle fill="#DB92D0" cx="327" cy="397.5" r="10.5"></circle>
|
||||
<circle fill="#DD99B8" cx="296" cy="392.5" r="10.5"></circle>
|
||||
</g>
|
||||
<g transform="translate(0,512) scale(0.1,-0.1)" fill="#000000" class="icon_syui">
|
||||
<path class="syui" d="M3660 4460 c-11 -11 -33 -47 -48 -80 l-29 -60 -12 38 c-27 88 -58 92 -98 11 -35 -70 -73 -159 -73 -169 0 -6 -5 -10 -10 -10 -6 0 -15 -10 -21 -22 -33 -73 -52 -92 -47 -48 2 26 -1 35 -14 38 -16 3 -168 -121 -168 -138 0 -5 -13 -16 -28 -24 -24 -13 -35 -12 -87 0 -221 55 -231 56 -480 56 -219 1 -247 -1 -320 -22 -44 -12 -96 -26 -115 -30 -57 -13 -122 -39 -200 -82 -8 -4 -31 -14 -50 -23 -41 -17 -34 -13 -146 -90 -87 -59 -292 -252 -351 -330 -63 -83 -143 -209 -143 -225 0 -10 -7 -23 -15 -30 -8 -7 -15 -17 -15 -22 0 -5 -13 -37 -28 -71 -16 -34 -36 -93 -45 -132 -9 -38 -24 -104 -34 -145 -13 -60 -17 -121 -17 -300 1 -224 1 -225 36 -365 24 -94 53 -175 87 -247 28 -58 51 -108 51 -112 0 -3 13 -24 28 -48 42 -63 46 -79 22 -85 -11 -3 -20 -9 -20 -14 0 -5 -4 -9 -10 -9 -5 0 -22 -11 -37 -25 -16 -13 -75 -59 -133 -100 -58 -42 -113 -82 -123 -90 -9 -8 -22 -15 -27 -15 -6 0 -10 -6 -10 -13 0 -8 -11 -20 -25 -27 -34 -18 -34 -54 0 -48 14 3 25 2 25 -1 0 -3 -43 -31 -95 -61 -52 -30 -95 -58 -95 -62 0 -5 -5 -8 -11 -8 -19 0 -84 -33 -92 -47 -4 -7 -15 -13 -22 -13 -14 0 -17 -4 -19 -32 -1 -8 15 -15 37 -18 l38 -5 -47 -48 c-56 -59 -54 -81 9 -75 30 3 45 0 54 -11 9 -13 16 -14 43 -4 29 11 30 10 18 -5 -7 -9 -19 -23 -25 -30 -7 -7 -13 -20 -13 -29 0 -12 8 -14 38 -9 20 4 57 8 82 9 25 2 54 8 66 15 18 10 23 8 32 -13 17 -38 86 -35 152 6 27 17 50 34 50 38 0 16 62 30 85 19 33 -15 72 -2 89 30 8 15 31 43 51 62 35 34 38 35 118 35 77 0 85 2 126 33 24 17 52 32 61 32 9 0 42 18 73 40 30 22 61 40 69 40 21 0 88 -26 100 -38 7 -7 17 -12 24 -12 7 0 35 -11 62 -25 66 -33 263 -84 387 -101 189 -25 372 -12 574 41 106 27 130 37 261 97 41 20 80 37 85 39 6 2 51 31 100 64 166 111 405 372 489 534 10 20 22 43 27 51 5 8 12 22 15 30 3 8 17 40 31 70 54 115 95 313 108 520 13 200 -43 480 -134 672 -28 58 -51 108 -51 112 0 3 -13 24 -29 48 -15 24 -34 60 -40 80 -19 57 3 142 50 193 10 11 22 49 28 85 6 36 16 67 21 68 18 6 31 53 25 83 -4 18 -17 33 -36 41 -16 7 -29 15 -29 18 1 10 38 50 47 50 5 0 20 11 33 25 18 19 22 31 17 61 -3 20 -14 45 -23 55 -16 18 -16 20 6 44 15 16 21 32 18 49 -3 15 1 34 8 43 32 43 7 73 -46 55 l-30 -11 0 85 c0 74 -2 84 -18 84 -21 0 -53 -33 -103 -104 l-34 -48 -5 74 c-7 102 -35 133 -80 88z m-870 -740 c36 -7 75 -14 88 -16 21 -4 23 -9 16 -37 -3 -18 -14 -43 -24 -57 -10 -14 -20 -35 -24 -46 -4 -12 -16 -32 -27 -45 -12 -13 -37 -49 -56 -79 -20 -30 -52 -73 -72 -96 -53 -60 -114 -133 -156 -189 -21 -27 -44 -54 -52 -58 -7 -4 -13 -14 -13 -22 0 -7 -18 -33 -40 -57 -22 -23 -40 -46 -40 -50 0 -5 -19 -21 -42 -38 -47 -35 -85 -38 -188 -15 -115 25 -173 20 -264 -23 -45 -22 -106 -46 -136 -56 -48 -15 -77 -25 -140 -50 -70 -28 -100 -77 -51 -84 14 -2 34 -10 45 -17 12 -7 53 -16 91 -20 90 -9 131 -22 178 -57 20 -16 52 -35 70 -43 18 -7 40 -22 49 -32 16 -18 15 -22 -24 -88 -23 -39 -47 -74 -53 -80 -7 -5 -23 -26 -36 -45 -26 -39 -92 -113 -207 -232 -4 -4 -37 -36 -73 -71 l-66 -64 -20 41 c-58 119 -105 240 -115 301 -40 244 -35 409 20 595 8 30 21 66 28 80 7 14 24 54 38 89 15 35 35 75 46 89 11 13 20 31 20 38 0 8 3 14 8 14 4 0 16 16 27 36 24 45 221 245 278 281 23 15 44 30 47 33 20 20 138 78 250 123 61 24 167 50 250 61 60 7 302 -1 370 -14z m837 -661 c52 -101 102 -279 106 -379 2 -42 0 -45 -28 -51 -16 -4 -101 -7 -187 -8 -166 -1 -229 10 -271 49 -19 19 -19 19 14 49 22 21 44 31 65 31 41 0 84 34 84 66 0 30 12 55 56 112 19 25 37 65 44 95 11 51 53 111 74 104 6 -2 25 -32 43 -68z m-662 -810 c17 -10 40 -24 53 -30 12 -7 22 -16 22 -20 0 -4 17 -13 38 -19 20 -7 44 -18 52 -24 8 -7 33 -21 55 -31 22 -11 42 -23 45 -26 11 -14 109 -49 164 -58 62 -11 101 -7 126 14 15 14 38 18 78 16 39 -2 26 -41 -49 -146 -78 -109 -85 -118 -186 -219 -61 -61 -239 -189 -281 -203 -17 -5 -73 -29 -104 -44 -187 -92 -605 -103 -791 -21 -42 19 -47 24 -37 41 5 11 28 32 51 48 22 15 51 38 64 51 13 12 28 22 33 22 17 0 242 233 242 250 0 6 5 10 10 10 6 0 10 6 10 14 0 25 50 55 100 62 59 8 56 6 115 83 50 66 74 117 75 162 0 14 7 40 16 57 18 38 52 41 99 11z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<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 class="ask-ai-btn" onclick="AskAI.toggle()" id="askAiButton">
|
||||
<span class="ai-icon icon-ai"></span>
|
||||
ai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Ask AI panel on all pages -->
|
||||
<!-- Ask AI Panel -->
|
||||
<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>
|
||||
<button onclick="AskAI.ask()" id="askButton">Ask</button>
|
||||
</div>
|
||||
|
||||
<!-- Chat history -->
|
||||
<div id="chatHistory" class="chat-history" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,309 +82,15 @@
|
||||
</div>
|
||||
|
||||
<footer class="main-footer">
|
||||
<p>© {{ config.author }}</p>
|
||||
<div class="footer-social">
|
||||
<a href="https://web.syu.is/@syui" target="_blank"><i class="fab fa-bluesky"></i></a>
|
||||
<a href="https://git.syui.ai/ai" target="_blank"><span class="icon-ai"></span></a>
|
||||
<a href="https://git.syui.ai/syui" target="_blank"><span class="icon-git"></span></a>
|
||||
</div>
|
||||
<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>
|
||||
<script src="/js/ask-ai.js"></script>
|
||||
<script src="/js/theme.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -27,7 +27,7 @@
|
||||
<div class="post-actions">
|
||||
<a href="{{ post.url }}" class="read-more">Read more</a>
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="view-markdown" title="View Markdown">Markdown</a>
|
||||
<a href="{{ post.markdown_url }}" class="view-markdown" title="View Markdown">.md</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
<a href="{{ post.translation_url }}" class="view-translation" title="View Translation">🌐</a>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<!-- OAuth Comment System - Load globally for session management -->
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-CDastf61.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-B330B6QX.css">
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-Do1JWJCw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-CPKYAM8U.css">
|
@@ -16,7 +16,7 @@
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
📝 Markdown
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
📝 Markdown
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
Markdown
|
||||
.md
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Production environment variables
|
||||
VITE_APP_HOST=https://log.syui.ai
|
||||
VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
||||
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
||||
VITE_APP_HOST=https://syui.ai
|
||||
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
|
||||
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
|
||||
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||
|
||||
# Collection names for OAuth app
|
||||
@@ -18,11 +18,10 @@ AILOG_COLLECTION_CHAT=ai.syui.log.chat
|
||||
VITE_AI_ENABLED=true
|
||||
VITE_AI_ASK_AI=true
|
||||
VITE_AI_PROVIDER=ollama
|
||||
VITE_AI_MODEL=gemma3:4b
|
||||
VITE_AI_MODEL=gemma3:2b
|
||||
VITE_AI_HOST=https://ollama.syui.ai
|
||||
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
||||
VITE_AI_SYSTEM_PROMPT="you are a helpful ai assistant trained on this blog's content. you can answer questions about the articles, provide insights, and help users understand the topics discussed."
|
||||
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
|
||||
|
||||
# API Configuration
|
||||
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
|
||||
|
||||
|
@@ -162,11 +162,17 @@
|
||||
}
|
||||
|
||||
.app-main {
|
||||
max-width: 1200px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.app .app-main {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.gacha-section {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
@@ -264,7 +270,7 @@
|
||||
.comment-section {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
/* padding: 20px; - removed to avoid double padding */
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
|
@@ -223,7 +223,11 @@ function App() {
|
||||
|
||||
const generatePlaceholderAvatar = (handle: string): string => {
|
||||
const initial = handle ? handle.charAt(0).toUpperCase() : 'U';
|
||||
return `https://via.placeholder.com/48x48/1185fe/ffffff?text=${initial}`;
|
||||
const svg = `<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" fill="#1185fe"/>
|
||||
<text x="24" y="32" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="white" text-anchor="middle">${initial}</text>
|
||||
</svg>`;
|
||||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
||||
};
|
||||
|
||||
const loadAiChatHistory = async (did: string) => {
|
||||
|
@@ -4,20 +4,32 @@ use colored::Colorize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub async fn execute(title: String, format: String) -> Result<()> {
|
||||
pub async fn execute(title: String, slug: Option<String>, format: String) -> Result<()> {
|
||||
println!("{} {}", "Creating new post:".green(), title);
|
||||
|
||||
let date = Local::now();
|
||||
|
||||
// Use provided slug or generate from title
|
||||
let slug_part = slug.unwrap_or_else(|| {
|
||||
title
|
||||
.to_lowercase()
|
||||
.replace(' ', "-")
|
||||
.chars()
|
||||
.filter(|c| c.is_alphanumeric() || *c == '-')
|
||||
.collect()
|
||||
});
|
||||
|
||||
let filename = format!(
|
||||
"{}-{}.{}",
|
||||
date.format("%Y-%m-%d"),
|
||||
title.to_lowercase().replace(' ', "-"),
|
||||
slug_part,
|
||||
format
|
||||
);
|
||||
|
||||
let content = format!(
|
||||
r#"---
|
||||
title: "{}"
|
||||
slug: "{}"
|
||||
date: {}
|
||||
tags: []
|
||||
draft: false
|
||||
@@ -28,6 +40,7 @@ draft: false
|
||||
Write your content here...
|
||||
"#,
|
||||
title,
|
||||
slug_part,
|
||||
date.format("%Y-%m-%d"),
|
||||
title
|
||||
);
|
||||
|
@@ -86,6 +86,21 @@ pub async fn build(project_dir: PathBuf) -> Result<()> {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("https://ollama.syui.ai");
|
||||
|
||||
let ai_system_prompt = ai_config
|
||||
.and_then(|ai| ai.get("system_prompt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("you are a helpful ai assistant");
|
||||
|
||||
let ai_did = ai_config
|
||||
.and_then(|ai| ai.get("ai_did"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("did:plc:4hqjfn7m6n5hno3doamuhgef");
|
||||
|
||||
// Extract bsky_api from oauth config
|
||||
let bsky_api = oauth_config.get("bsky_api")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("https://public.api.bsky.app");
|
||||
|
||||
// 4. Create .env.production content
|
||||
let env_content = format!(
|
||||
r#"# Production environment variables
|
||||
@@ -110,6 +125,11 @@ VITE_AI_ASK_AI={}
|
||||
VITE_AI_PROVIDER={}
|
||||
VITE_AI_MODEL={}
|
||||
VITE_AI_HOST={}
|
||||
VITE_AI_SYSTEM_PROMPT="{}"
|
||||
VITE_AI_DID={}
|
||||
|
||||
# API Configuration
|
||||
VITE_BSKY_PUBLIC_API={}
|
||||
"#,
|
||||
base_url,
|
||||
base_url, client_id_path,
|
||||
@@ -125,7 +145,10 @@ VITE_AI_HOST={}
|
||||
ai_ask_ai,
|
||||
ai_provider,
|
||||
ai_model,
|
||||
ai_host
|
||||
ai_host,
|
||||
ai_system_prompt,
|
||||
ai_did,
|
||||
bsky_api
|
||||
);
|
||||
|
||||
// 5. Find oauth directory (relative to current working directory)
|
||||
|
@@ -23,6 +23,7 @@ pub struct SiteConfig {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BuildConfig {
|
||||
pub highlight_code: bool,
|
||||
pub highlight_theme: Option<String>,
|
||||
pub minify: bool,
|
||||
}
|
||||
|
||||
@@ -146,6 +147,7 @@ impl Default for Config {
|
||||
},
|
||||
build: BuildConfig {
|
||||
highlight_code: true,
|
||||
highlight_theme: Some("Monokai".to_string()),
|
||||
minify: false,
|
||||
},
|
||||
ai: Some(AiConfig {
|
||||
|
@@ -18,7 +18,7 @@ pub struct Generator {
|
||||
|
||||
impl Generator {
|
||||
pub fn new(base_path: PathBuf, config: Config) -> Result<Self> {
|
||||
let markdown_processor = MarkdownProcessor::new(config.build.highlight_code);
|
||||
let markdown_processor = MarkdownProcessor::new(config.build.highlight_code, config.build.highlight_theme.clone());
|
||||
let template_engine = TemplateEngine::new(base_path.join("templates"))?;
|
||||
|
||||
let ai_manager = if let Some(ref ai_config) = config.ai {
|
||||
@@ -40,6 +40,20 @@ impl Generator {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_config_with_timestamp(&self) -> Result<serde_json::Value> {
|
||||
let mut config_with_timestamp = serde_json::to_value(&self.config.site)?;
|
||||
if let Some(config_obj) = config_with_timestamp.as_object_mut() {
|
||||
config_obj.insert("build_timestamp".to_string(), serde_json::Value::String(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.to_string()
|
||||
));
|
||||
}
|
||||
Ok(config_with_timestamp)
|
||||
}
|
||||
|
||||
pub async fn build(&self) -> Result<()> {
|
||||
// Clean public directory
|
||||
let public_dir = self.base_path.join("public");
|
||||
@@ -184,16 +198,17 @@ impl Generator {
|
||||
|
||||
let html_content = self.markdown_processor.render(&content)?;
|
||||
|
||||
// Use slug from frontmatter if available, otherwise derive from filename
|
||||
let slug = frontmatter.get("slug")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
path.file_stem()
|
||||
// Use filename (without extension) as URL slug to include date
|
||||
let filename_slug = path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("post")
|
||||
.to_string()
|
||||
});
|
||||
.to_string();
|
||||
|
||||
// Still keep the slug field from frontmatter for other purposes
|
||||
let frontmatter_slug = frontmatter.get("slug")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| filename_slug.clone());
|
||||
|
||||
let mut post = Post {
|
||||
title: frontmatter.get("title")
|
||||
@@ -205,8 +220,9 @@ impl Generator {
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
content: html_content,
|
||||
slug: slug.clone(),
|
||||
url: format!("/posts/{}.html", slug),
|
||||
slug: frontmatter_slug.clone(),
|
||||
filename_slug: filename_slug.clone(),
|
||||
url: format!("/posts/{}.html", filename_slug),
|
||||
tags: frontmatter.get("tags")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter()
|
||||
@@ -233,7 +249,7 @@ impl Generator {
|
||||
lang: "en".to_string(),
|
||||
title: translated_title,
|
||||
content: translated_html,
|
||||
url: format!("/posts/{}-en.html", post.slug),
|
||||
url: format!("/posts/{}-en.html", post.filename_slug),
|
||||
}]);
|
||||
}
|
||||
Err(e) => eprintln!("Translation failed: {}", e),
|
||||
@@ -259,7 +275,7 @@ impl Generator {
|
||||
// Enhance posts with additional metadata for timeline view
|
||||
let enhanced_posts: Vec<serde_json::Value> = posts.iter().map(|post| {
|
||||
let excerpt = self.extract_excerpt(&post.content);
|
||||
let markdown_url = format!("/posts/{}.md", post.slug);
|
||||
let markdown_url = format!("/posts/{}.md", post.filename_slug);
|
||||
let translation_url = if let Some(ref translations) = post.translations {
|
||||
translations.first().map(|t| t.url.clone())
|
||||
} else {
|
||||
@@ -281,7 +297,8 @@ impl Generator {
|
||||
}).collect();
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("config", &self.config.site);
|
||||
let config_with_timestamp = self.create_config_with_timestamp()?;
|
||||
context.insert("config", &config_with_timestamp);
|
||||
context.insert("posts", &enhanced_posts);
|
||||
|
||||
let html = self.template_engine.render("index.html", &context)?;
|
||||
@@ -294,14 +311,15 @@ impl Generator {
|
||||
|
||||
async fn generate_post_page(&self, post: &Post) -> Result<()> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("config", &self.config.site);
|
||||
let config_with_timestamp = self.create_config_with_timestamp()?;
|
||||
context.insert("config", &config_with_timestamp);
|
||||
|
||||
// Create enhanced post with additional URLs
|
||||
let mut enhanced_post = post.clone();
|
||||
enhanced_post.url = format!("/posts/{}.html", post.slug);
|
||||
enhanced_post.url = format!("/posts/{}.html", post.filename_slug);
|
||||
|
||||
// Add markdown view URL
|
||||
let markdown_url = format!("/posts/{}.md", post.slug);
|
||||
let markdown_url = format!("/posts/{}.md", post.filename_slug);
|
||||
|
||||
// Add translation URLs if available
|
||||
let translation_urls: Vec<String> = if let Some(ref translations) = post.translations {
|
||||
@@ -328,7 +346,7 @@ impl Generator {
|
||||
let output_dir = self.base_path.join("public/posts");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let output_path = output_dir.join(format!("{}.html", post.slug));
|
||||
let output_path = output_dir.join(format!("{}.html", post.filename_slug));
|
||||
fs::write(output_path, html)?;
|
||||
|
||||
// Generate markdown view
|
||||
@@ -339,7 +357,8 @@ impl Generator {
|
||||
|
||||
async fn generate_translation_page(&self, post: &Post, translation: &Translation) -> Result<()> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("config", &self.config.site);
|
||||
let config_with_timestamp = self.create_config_with_timestamp()?;
|
||||
context.insert("config", &config_with_timestamp);
|
||||
context.insert("post", &TranslatedPost {
|
||||
title: translation.title.clone(),
|
||||
date: post.date.clone(),
|
||||
@@ -356,7 +375,7 @@ impl Generator {
|
||||
let output_dir = self.base_path.join("public/posts");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let output_path = output_dir.join(format!("{}-{}.html", post.slug, translation.lang));
|
||||
let output_path = output_dir.join(format!("{}-{}.html", post.filename_slug, translation.lang));
|
||||
fs::write(output_path, html)?;
|
||||
|
||||
Ok(())
|
||||
@@ -413,11 +432,11 @@ impl Generator {
|
||||
.unwrap_or("")
|
||||
});
|
||||
|
||||
if file_slug == post.slug {
|
||||
if file_slug == post.slug || path.file_stem().and_then(|s| s.to_str()).unwrap_or("") == post.filename_slug {
|
||||
let output_dir = self.base_path.join("public/posts");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let output_path = output_dir.join(format!("{}.md", post.slug));
|
||||
let output_path = output_dir.join(format!("{}.md", post.filename_slug));
|
||||
fs::write(output_path, content)?;
|
||||
break;
|
||||
}
|
||||
@@ -447,6 +466,7 @@ pub struct Post {
|
||||
pub date: String,
|
||||
pub content: String,
|
||||
pub slug: String,
|
||||
pub filename_slug: String, // Added for URL generation
|
||||
pub url: String,
|
||||
pub tags: Vec<String>,
|
||||
pub translations: Option<Vec<Translation>>,
|
||||
|
@@ -42,6 +42,9 @@ enum Commands {
|
||||
New {
|
||||
/// Title of the post
|
||||
title: String,
|
||||
/// Slug for the post (optional, derived from title if not provided)
|
||||
#[arg(short, long)]
|
||||
slug: Option<String>,
|
||||
/// Post format
|
||||
#[arg(short, long, default_value = "md")]
|
||||
format: String,
|
||||
@@ -140,9 +143,9 @@ async fn main() -> Result<()> {
|
||||
Commands::Build { path } => {
|
||||
commands::build::execute(path).await?;
|
||||
}
|
||||
Commands::New { title, format, path } => {
|
||||
Commands::New { title, slug, format, path } => {
|
||||
std::env::set_current_dir(path)?;
|
||||
commands::new::execute(title, format).await?;
|
||||
commands::new::execute(title, slug, format).await?;
|
||||
}
|
||||
Commands::Serve { port, path } => {
|
||||
std::env::set_current_dir(path)?;
|
||||
|
@@ -9,14 +9,16 @@ use serde_json::Value;
|
||||
|
||||
pub struct MarkdownProcessor {
|
||||
highlight_code: bool,
|
||||
highlight_theme: String,
|
||||
syntax_set: SyntaxSet,
|
||||
theme_set: ThemeSet,
|
||||
}
|
||||
|
||||
impl MarkdownProcessor {
|
||||
pub fn new(highlight_code: bool) -> Self {
|
||||
pub fn new(highlight_code: bool, highlight_theme: Option<String>) -> Self {
|
||||
Self {
|
||||
highlight_code,
|
||||
highlight_theme: highlight_theme.unwrap_or_else(|| "Monokai".to_string()),
|
||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
}
|
||||
@@ -86,14 +88,19 @@ impl MarkdownProcessor {
|
||||
let parser = Parser::new_ext(content, options);
|
||||
let mut html_output = String::new();
|
||||
let mut code_block = None;
|
||||
let theme = &self.theme_set.themes["base16-ocean.dark"];
|
||||
// Force use dark theme for better visibility on dark background
|
||||
let theme = self.theme_set.themes.get("base16-monokai.dark")
|
||||
.or_else(|| self.theme_set.themes.get("base16-ocean.dark"))
|
||||
.or_else(|| self.theme_set.themes.get("Solarized (dark)"))
|
||||
.or_else(|| self.theme_set.themes.get(&self.highlight_theme))
|
||||
.unwrap_or_else(|| self.theme_set.themes.values().next().unwrap());
|
||||
|
||||
let mut events = Vec::new();
|
||||
for event in parser {
|
||||
match event {
|
||||
pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(kind)) => {
|
||||
if let CodeBlockKind::Fenced(lang) = &kind {
|
||||
code_block = Some((String::new(), lang.to_string()));
|
||||
if let CodeBlockKind::Fenced(lang_info) = &kind {
|
||||
code_block = Some((String::new(), lang_info.to_string()));
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Event::Text(text) => {
|
||||
@@ -104,8 +111,8 @@ impl MarkdownProcessor {
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Event::End(pulldown_cmark::TagEnd::CodeBlock) => {
|
||||
if let Some((code, lang)) = code_block.take() {
|
||||
let highlighted = self.highlight_code_block(&code, &lang, theme);
|
||||
if let Some((code, lang_info)) = code_block.take() {
|
||||
let highlighted = self.highlight_code_block(&code, &lang_info, theme);
|
||||
events.push(pulldown_cmark::Event::Html(highlighted.into()));
|
||||
}
|
||||
}
|
||||
@@ -117,13 +124,41 @@ impl MarkdownProcessor {
|
||||
Ok(html_output)
|
||||
}
|
||||
|
||||
fn highlight_code_block(&self, code: &str, lang: &str, theme: &syntect::highlighting::Theme) -> String {
|
||||
fn highlight_code_block(&self, code: &str, lang_info: &str, theme: &syntect::highlighting::Theme) -> String {
|
||||
// Parse language and filename from lang_info (e.g., "sh:/path/to/file" or "rust:main.rs")
|
||||
let (lang, filename) = if lang_info.contains(':') {
|
||||
let parts: Vec<&str> = lang_info.splitn(2, ':').collect();
|
||||
(parts[0], Some(parts[1]))
|
||||
} else {
|
||||
(lang_info, None)
|
||||
};
|
||||
|
||||
// Map short language names to full names
|
||||
let lang = match lang {
|
||||
"rs" => "rust",
|
||||
"js" => "javascript",
|
||||
"ts" => "typescript",
|
||||
"sh" => "bash",
|
||||
"yml" => "yaml",
|
||||
"md" => "markdown",
|
||||
"py" => "python",
|
||||
_ => lang,
|
||||
};
|
||||
|
||||
let syntax = self.syntax_set
|
||||
.find_syntax_by_token(lang)
|
||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
|
||||
|
||||
let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
|
||||
let mut output = String::from("<pre><code>");
|
||||
|
||||
// Create pre tag with optional filename attribute
|
||||
let pre_tag = if let Some(filename) = filename {
|
||||
format!("<pre data-filename=\"{}\">", filename)
|
||||
} else {
|
||||
"<pre>".to_string()
|
||||
};
|
||||
|
||||
let mut output = format!("{}<code>", pre_tag);
|
||||
|
||||
for line in code.lines() {
|
||||
let ranges = highlighter.highlight_line(line, &self.syntax_set).unwrap();
|
||||
|
22
systemd/system/ailog-stream.service
Normal file
22
systemd/system/ailog-stream.service
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=ailog stream monitoring service
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=syui
|
||||
Group=syui
|
||||
WorkingDirectory=/home/syui/git/log
|
||||
ExecStart=/home/syui/.cargo/bin/ailog stream start my-blog
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Environment variables if needed
|
||||
Environment=RUST_LOG=info
|
||||
Environment=AILOG_DEBUG_ALL=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
25
systemd/system/cloudflared-log.service
Normal file
25
systemd/system/cloudflared-log.service
Normal file
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=Cloudflared tunnel for log.syui.ai
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=syui
|
||||
Group=syui
|
||||
WorkingDirectory=/home/syui/git/log
|
||||
ExecStart=/usr/bin/cloudflared tunnel --config /home/syui/git/log/cloudflared-config.yml run
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/syui/git/log
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Reference in New Issue
Block a user