144 Commits

Author SHA1 Message Date
cb8b0582e9 rm log 2025-08-01 21:25:52 +09:00
85494944ad rm log 2025-08-01 20:36:35 +09:00
5aeeba106a add post 2025-07-30 19:30:01 +09:00
f1e76ab31f fix post 2025-07-27 05:04:01 +09:00
3c9ef78696 add binary 2025-07-26 20:54:23 +09:00
ee2d21b0f3 update 2025-07-26 20:00:16 +09:00
0667ac58fb test game 2025-07-26 19:51:55 +09:00
d89855338b fix css 2025-07-18 10:57:42 +09:00
e19170cdff add pds.html 2025-07-18 00:05:04 +09:00
c3e22611f5 fix layout 2025-07-17 23:57:08 +09:00
2943c94ec1 binary 2025-07-17 22:23:14 +09:00
f27997b7e8 rm pds asset 2025-07-17 22:20:25 +09:00
447e4bded9 update 2025-07-17 22:12:06 +09:00
03161a52ca fix oauth-ai-chat 2025-07-17 19:26:40 +09:00
fe9381a860 fix blog post 2025-07-17 19:26:40 +09:00
f0cea89005 fix oauth filter 2025-07-16 22:57:09 +09:00
b059fe1de0 fix comment, rm console.log 2025-07-16 22:53:01 +09:00
07b0b0f702 fix css 2025-07-16 20:58:42 +09:00
ecd69557fe oauth markdown 2025-07-16 20:42:50 +09:00
452a0fda6a fix blog post 2025-07-16 11:47:15 +09:00
a62dd82790 fix config 2025-07-16 11:27:37 +09:00
3faec33bac fix blog post 2025-07-16 11:04:50 +09:00
33402f4a21 add blog post 2025-07-16 11:04:02 +09:00
3e65bc8210 binary 2025-07-16 10:18:03 +09:00
16d724ec25 update 2025-07-16 10:08:43 +09:00
69182a1bf8 update 2025-07-16 09:33:46 +09:00
0110773592 test ai-blog 2025-07-16 09:32:45 +09:00
75f108e7b8 fix blog post link 2025-07-14 15:27:10 +09:00
263189ce72 add blog post 2025-07-14 14:11:55 +09:00
7800a655f3 fix profile 2025-07-13 08:12:40 +09:00
76c797e4d8 add blog post 2025-07-13 07:52:43 +09:00
d1a1c92842 update binary 2025-07-11 13:38:22 +09:00
9da1f87640 fix update version 2025-07-11 13:09:15 +09:00
ddfc43512c add md msg 2025-07-11 08:52:34 +09:00
b3ccd61935 add my-blog msg 2025-07-11 08:51:46 +09:00
a243b6a44e fix post filename 2025-07-05 15:42:36 +09:00
e3c1cf4790 fix build err 2025-07-05 15:31:04 +09:00
a6236661bf post 2025-07-05 15:30:55 +09:00
195a4474c9 fix config.toml 2025-07-01 21:22:48 +09:00
4a34a6ca59 rm my-blog/oauth 2025-07-01 21:20:26 +09:00
4d01fb8507 fix oauth network err 2025-07-01 19:48:49 +09:00
d69c9aa09b update binary 2025-07-01 06:22:15 +09:00
99ee49f76e feat: add server-side image comparison shortcode support
- Add {{< img-compare >}} and [img-compare] shortcode syntax
- Implement server-side shortcode processing in Rust
- Create dedicated shortcode module for extensibility
- Fix image comparison slider display issues
- Remove caption display for cleaner UI
- Update to version 0.2.6

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-01 06:09:39 +09:00
19c0e28668 add post 2025-07-01 06:02:31 +09:00
bc99eb0814 update img-slider 2025-07-01 06:02:25 +09:00
cf93721bad fix social-app uri 2025-06-26 19:56:13 +09:00
8a8a121f4a fix delete record 2025-06-25 23:14:27 +09:00
be2bcae1d6 fix test ask-AI oauth profile 2025-06-25 23:03:50 +09:00
2c08a4acfb test blog profile 2025-06-25 21:18:13 +09:00
7791399314 fix claude-code proxy 2025-06-24 22:55:16 +09:00
26b1b2cf87 fix mobile css 2025-06-22 01:50:49 +09:00
7eb653f569 fix layout article.article-content 2025-06-22 01:16:59 +09:00
0fc920c844 fix layout 2025-06-22 00:35:54 +09:00
13c05d97d2 add claude-code-mcp-server 2025-06-22 00:26:20 +09:00
71acd44810 fix layout font-size 2025-06-22 00:25:44 +09:00
1b4579d0f1 fix layout font-size 2025-06-22 00:25:04 +09:00
09100f6d99 fix ask-ai prompt userdata 2025-06-22 00:01:27 +09:00
169de9064a fix link github 2025-06-21 19:11:01 +09:00
097c794623 fix oauth bsky button 2025-06-21 18:30:39 +09:00
b652e01dd3 fix oauth loading button 2025-06-21 17:03:23 +09:00
31af524303 fix layout 2025-06-21 15:46:21 +09:00
6be024864d cleanup docs 2025-06-21 00:07:22 +09:00
eef1fdad38 fix layout 2025-06-20 23:26:32 +09:00
b7e411e8b2 add img 2025-06-20 23:26:11 +09:00
8f9d803a94 fix gh-actions 2025-06-20 00:08:23 +09:00
f9b9c2ab52 fix layout 2025-06-20 00:00:51 +09:00
210ce801f1 update binary 2025-06-20 00:00:29 +09:00
6cb46f2ca1 fix token refresh 2025-06-19 23:01:41 +09:00
9406597b82 add post 2025-06-19 22:08:43 +09:00
0dbc3ba67e fix html text 2025-06-19 21:22:01 +09:00
a7e6fc4a1a Release v0.2.4: Complete OAuth system with AI chat and mobile support
- Fixed OAuth authentication with ATProto integration
- Implemented Ask AI functionality with conversation history
- Resolved PDS/web link issues for cross-network compatibility
- Added comprehensive mobile responsive design
- Enhanced comment posting with loading states and auto-refresh
- Improved chat record display with question/answer pairing
- Fixed tab scrolling and layout overflow issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-19 20:02:24 +09:00
3adcfdacf5 fix post commnet 2025-06-19 19:58:40 +09:00
004081337c fix ask-ai put 2025-06-19 19:52:31 +09:00
5ce0e0fd7a fix ask-ai 2025-06-19 19:18:50 +09:00
f816abb84f fix mobile css, ask-ai 2025-06-19 19:12:29 +09:00
8541af9293 add binary 2025-06-19 17:26:48 +09:00
68b49d5aaf Fix jetstream monitoring for ai.syui.log collections
- Fixed JetstreamMessage struct to correctly parse collection from commit object
- Fixed user list JSON format to match oauth app expectations (removed metadata field)
- Added monitoring for both ai.syui.log and ai.syui.log.chat.comment collections
- Improved error handling and debug output for stream processing
- Jetstream auto user registration now working correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-19 17:14:11 +09:00
53dab3fd09 fix ailog stream server 2025-06-19 17:13:02 +09:00
5fac689f98 fix hugo callback 2025-06-19 17:13:02 +09:00
293421b7a5 add callback test 2025-06-19 14:47:25 +09:00
1793de40c1 fix rm callback page 2025-06-19 13:17:32 +09:00
30bdd7b633 fix oauth package name 2025-06-19 13:09:37 +09:00
b17ac3d91a v0.2.2: OAuth authentication system improvements
🔧 OAuth Fixes:
- Add transition:generic scope to resolve authentication errors
- Improve Agent creation with session object and dpopFetch fallback
- Fix avatar fetching to use correct public API endpoints
- Proper PDS endpoint selection (bsky.syu.is vs public.api.bsky.app)

🎨 UI Improvements:
- Remove 'Demo' text from loading states
- Environment-based feature toggles (TestUI/Debug)
- Unified padding system (20px 0)
- CSS conflict resolution with oauth- prefix

🚀 Production Ready:
- Automatic feature disable in production build
- Session management improvements
- Error handling enhancements

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-19 11:56:06 +09:00
81f87d0462 fix loading 2025-06-19 11:41:01 +09:00
a020fa24d8 fix gh-actions oauth-session 2025-06-19 11:34:56 +09:00
21c53010b7 test oauth_new gh-actions 2025-06-19 11:13:11 +09:00
4f7834f85c fix npm env production 2025-06-18 19:16:25 +09:00
fecd927b91 fix oauth_new env test 2025-06-18 18:16:37 +09:00
b54e8089ea oauth_new 2025-06-18 17:25:42 +09:00
174cb12d4d test merge 2025-06-18 10:53:48 +09:00
a1186f8185 Merge branch 'test-oauth' 2025-06-18 10:53:31 +09:00
833549756b fix did check 2025-06-17 22:36:33 +09:00
4edde5293a Add oauth_new: Complete OAuth authentication and comment system
- Created new oauth_new directory with clean OAuth implementation
- Added 4-tab interface: Lang, Comment, Collection, User Comments
- Implemented OAuth authentication with @atproto/oauth-client-browser
- Added comment posting functionality with putRecord
- Added proper PDS detection and error handling
- Skipped placeholder users to prevent errors
- Built comprehensive documentation (README.md, DEVELOPMENT.md)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 22:34:03 +09:00
f0fdf678c8 fix oauth plc 2025-06-17 17:43:03 +09:00
820e47f634 update binary 2025-06-17 11:01:42 +09:00
4dac4a83e0 fix atproto web link 2025-06-17 11:00:09 +09:00
fccf75949c v0.2.1: Fix async trait implementation warnings
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 10:42:15 +09:00
6600a9e0cf test pds oauth did 2025-06-17 10:41:22 +09:00
0d79af5aa5 v0.2.0: Unified AI content display and OAuth PDS fixes
Major Changes:
- Unified AI content rendering across all collection types (chat, lang, comment)
- Fixed PDS endpoint detection and usage based on handle configuration
- Removed hardcoded 'yui.syui.ai' references and used environment variables
- Fixed OAuth app 400 errors by adding null checks for API calls
- Improved AI DID resolution to use correct ai.syui.ai account
- Fixed avatar and profile link generation for correct PDS routing
- Enhanced network configuration mapping for different PDS types

OAuth App Improvements:
- Consolidated renderAIContent() function for all AI collections
- Fixed generateProfileUrl() to use PDS-specific web URLs
- Removed duplicate AI content rendering code
- Added proper error handling for API calls

Technical Fixes:
- Updated stream.rs to use correct AI DID defaults
- Improved CORS handling for Ollama localhost connections
- Enhanced PDS detection logic for handle-based routing
- Cleaned up production code (removed console.log statements)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 01:51:11 +09:00
db04af76ab test cleanup 2025-06-17 01:48:30 +09:00
5f0b09b555 add binary 2025-06-16 22:48:38 +09:00
8fa9e474d1 v0.1.9: Production deployment ready
🚀 Production Features
- Console output cleanup: Removed all console.log/warn/error from OAuth app
- Clean UI: Removed debug info divs from production build
- Warning-free builds: Fixed all Rust compilation warnings

🔧 Authentication & Stream Improvements
- Enhanced password authentication with PDS specification support
- Fixed AI DID resolution: Now correctly uses ai.syui.ai (did:plc:6qyecktefllvenje24fcxnie)
- Improved project directory config loading for ailog stream commands
- Added user list initialization commands with proper PDS detection

📚 Documentation
- Complete command reference in docs/commands.md
- Architecture documentation in docs/architecture.md
- Getting started guide in docs/getting-started.md

🛠️ Technical Improvements
- Project-aware AI config loading from config.toml
- Runtime DID resolution for OAuth app
- Proper handle/DID distinction across all components
- Enhanced error handling with clean user feedback

🔐 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 22:29:46 +09:00
5339dd28b0 test scpt 2025-06-16 22:27:20 +09:00
1e83b50e3f test cli stream 2025-06-16 22:09:04 +09:00
889ce8baa1 test oauth pds 2025-06-16 20:45:55 +09:00
286b46c6e6 fix systemd 2025-06-16 12:17:42 +09:00
b780d27ace update binary 2025-06-16 12:17:29 +09:00
831fcb7865 v0.1.8: Enhanced OAuth search bar and configurable AI settings
- Transform auth-section to search bar layout (input left, button right)
- Change atproto button text to "@" symbol
- Add num_predict configuration in config.toml for AI response length
- Improve mobile responsiveness for auth section
- Remove auth-status section for cleaner UI

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 11:45:24 +09:00
3f8bbff7c2 fix layout oauth-bar 2025-06-16 11:43:15 +09:00
5cb73a9ed3 add scpt 2025-06-16 10:55:30 +09:00
6ce8d44c4b cleanup 2025-06-16 10:53:42 +09:00
167cfb35f7 fix tab name 2025-06-16 02:29:37 +09:00
c8377ceabf rm auth-status 2025-06-16 02:25:00 +09:00
e917c563f2 update layout 2025-06-16 02:21:26 +09:00
a76933c23b cleanup 2025-06-16 01:16:36 +09:00
8d960b7a40 fix ask-ai enter 2025-06-15 23:33:22 +09:00
d3967c782f rm html 2025-06-15 23:23:12 +09:00
63b6fd5142 fix ai handle 2025-06-15 23:21:15 +09:00
27935324c7 fix mobile css 2025-06-15 22:56:34 +09:00
594d7e7aef v0.1.7: Enhanced UI and accessibility improvements
- Add CSS styling for chat messages with theme color border
- Fix comment form visibility (only show on Comments tab)
- Remove comment form heading for cleaner UI
- Add accessibility attributes (id/name) to all form fields
- Fix Japanese input handling in Ask AI (prevent accidental submission during IME composition)
- Unified CSS classes across all content types (comments, AI chat, translations)
- Fix rkey filtering to handle .html extensions consistently

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-15 22:36:19 +09:00
be86c11e74 fix comment-tab 2025-06-15 22:34:44 +09:00
619675b551 fix post-page rkey 2025-06-15 22:22:01 +09:00
d4d98e2e91 v0.1.6: Major improvements to OAuth display and stream configuration
- Fix AI Chat History display layout and content formatting
- Unify comment layout structure across all comment types
- Remove hardcoded values from stream.rs, add config.toml support
- Optimize AI comment generation with character limits
- Improve translation length limits (3000 characters)
- Add comprehensive AI configuration management

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-15 22:22:01 +09:00
8dac463345 test update json 2025-06-15 22:22:01 +09:00
095f6ec386 v0.1.5: Unify collection configuration under VITE_OAUTH_COLLECTION
- Remove AILOG_OAUTH_COLLECTION backward compatibility
- Update stream.rs to use simplified collection structure
- Fix collection loading from project config.toml
- Resolve compiler warnings with #[allow(dead_code)]
- All systems now use unified VITE_OAUTH_COLLECTION variable

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-15 15:25:52 +09:00
c12d42882c test update 2025-06-15 15:23:32 +09:00
67b241f1e8 rm at-uri add post-url 2025-06-15 13:02:50 +09:00
4206b2195d fix post 2025-06-15 11:30:19 +09:00
b3c1b01e9e fix mobile css 2025-06-15 09:37:49 +09:00
ffa4fa0846 add scpt 2025-06-14 21:55:28 +09:00
0e75d4c0e6 fix comment input 2025-06-14 21:09:10 +09:00
b7f62e729a fix ask-AI 2025-06-14 20:48:17 +09:00
3b2c53fc97 Add GitHub Actions workflows and optimize build performance
- Add release.yml for multi-platform binary builds (Linux, macOS, Windows)
- Add gh-pages-fast.yml for fast deployment using pre-built binaries
- Add build-binary.yml for standalone binary artifact creation
- Optimize Cargo.toml with build profiles and reduced tokio features
- Remove 26MB of unused Font Awesome assets (kept only essential files)
- Font Awesome reduced from 28MB to 1.2MB

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-14 19:52:08 +09:00
13f1785081 update 2025-06-14 15:56:25 +09:00
bb6d51a602 fix css 2025-06-14 13:17:09 +09:00
a4114c5be3 fix theme 2025-06-14 13:17:09 +09:00
5c13dc0a1c fix readme 2025-06-14 13:17:09 +09:00
cef0675a88 add system 2025-06-14 13:17:09 +09:00
fd223290df code layout 2025-06-14 13:17:09 +09:00
5f4382911b fix command build 2025-06-14 13:17:09 +09:00
95cee69482 add github 2025-06-14 13:17:08 +09:00
33c166fa0c fix color 2025-06-14 13:17:08 +09:00
36863e4d9f fix loading 2025-06-14 13:17:08 +09:00
fb0e5107cf add ask AI 2025-06-14 13:17:08 +09:00
220 changed files with 19072 additions and 9423 deletions

View File

@@ -1,44 +0,0 @@
{
"permissions": {
"allow": [
"Bash(cargo init:*)",
"Bash(cargo:*)",
"Bash(find:*)",
"Bash(mkdir:*)",
"Bash(../target/debug/ailog new:*)",
"Bash(../target/debug/ailog build)",
"Bash(/Users/syui/ai/log/target/debug/ailog build)",
"Bash(ls:*)",
"Bash(curl:*)",
"Bash(pkill:*)",
"WebFetch(domain:docs.anthropic.com)",
"WebFetch(domain:github.com)",
"Bash(rm:*)",
"Bash(mv:*)",
"Bash(cp:*)",
"Bash(timeout:*)",
"Bash(grep:*)",
"Bash(./target/debug/ailog:*)",
"Bash(cat:*)",
"Bash(npm install)",
"Bash(npm run build:*)",
"Bash(chmod:*)",
"Bash(./scripts/tunnel.sh:*)",
"Bash(PRODUCTION=true cargo run -- build)",
"Bash(cloudflared tunnel:*)",
"Bash(npm install:*)",
"Bash(./scripts/build-oauth-partial.zsh:*)",
"Bash(./scripts/quick-oauth-update.zsh:*)",
"Bash(../target/debug/ailog serve)",
"Bash(./scripts/test-oauth.sh:*)",
"Bash(./run.zsh:*)",
"Bash(npm run dev:*)",
"Bash(./target/release/ailog:*)",
"Bash(rg:*)",
"Bash(../target/release/ailog build)",
"Bash(zsh run.zsh:*)",
"Bash(hugo:*)"
],
"deny": []
}
}

View File

@@ -0,0 +1,123 @@
name: Deploy to Cloudflare Pages
on:
push:
branches:
- main
workflow_dispatch:
env:
OAUTH_DIR: oauth
KEEP_DEPLOYMENTS: 5
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '21'
- name: Install dependencies
run: |
cd ${{ env.OAUTH_DIR }}
npm install
- name: Build OAuth app
run: |
cd ${{ env.OAUTH_DIR }}
NODE_ENV=production npm run build
- name: Copy OAuth build to static
run: |
rm -rf my-blog/static/assets
cp -rf ${{ env.OAUTH_DIR }}/dist/* my-blog/static/
cp ${{ env.OAUTH_DIR }}/dist/index.html my-blog/templates/oauth-assets.html
- name: Cache ailog binary
uses: actions/cache@v4
with:
path: ./bin
key: ailog-bin-${{ runner.os }}
restore-keys: |
ailog-bin-${{ runner.os }}
- name: Setup ailog binary
run: |
# Get expected version from Cargo.toml
EXPECTED_VERSION=$(grep '^version' Cargo.toml | cut -d'"' -f2)
echo "Expected version from Cargo.toml: $EXPECTED_VERSION"
# Check current binary version if exists
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Current binary version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Check OS
OS="${{ runner.os }}"
echo "Runner OS: $OS"
# Use pre-packaged binary if version matches or extract from tar.gz
if [ "$CURRENT_VERSION" = "$EXPECTED_VERSION" ]; then
echo "Binary is up to date"
chmod +x ./bin/ailog
elif [ "$OS" = "Linux" ] && [ -f "./bin/ailog-linux-x86_64.tar.gz" ]; then
echo "Extracting ailog from pre-packaged tar.gz..."
cd bin
tar -xzf ailog-linux-x86_64.tar.gz
chmod +x ailog
cd ..
# Verify extracted version
EXTRACTED_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Extracted binary version: $EXTRACTED_VERSION"
if [ "$EXTRACTED_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Warning: Binary version mismatch. Expected $EXPECTED_VERSION but got $EXTRACTED_VERSION"
fi
else
echo "Error: No suitable binary found for OS: $OS"
exit 1
fi
- name: Build site with ailog
run: |
cd my-blog
../bin/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
wranglerVersion: '3'
cleanup:
needs: deploy
runs-on: ubuntu-latest
if: success()
steps:
- name: Cleanup old deployments
run: |
curl -X PATCH \
"https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{ \"deployment_configs\": { \"production\": { \"deployment_retention\": ${{ env.KEEP_DEPLOYMENTS }} } } }"

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

View 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

View File

@@ -0,0 +1,193 @@
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 with Gitea API
run: |
# Prepare release files
mkdir -p release
find artifacts -name "*.tar.gz" -exec cp {} release/ \;
# Create release via Gitea API
RELEASE_RESPONSE=$(curl -X POST \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "${{ steps.tag_name.outputs.tag }}",
"name": "ailog ${{ steps.tag_name.outputs.tag }}",
"body": "'"$(cat release_notes.md | sed 's/"/\\"/g' | tr '\n' ' ')"'",
"draft": false,
"prerelease": '"$(if echo "${{ steps.tag_name.outputs.tag }}" | grep -E "(alpha|beta|rc)"; then echo "true"; else echo "false"; fi)"'
}')
# Get release ID
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
echo "Created release with ID: $RELEASE_ID"
# Upload release assets
for file in release/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
echo "Uploading $filename..."
curl -X POST \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=$filename" \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$file"
fi
done

View File

@@ -6,6 +6,10 @@ on:
- main
workflow_dispatch:
env:
OAUTH_DIR: oauth
KEEP_DEPLOYMENTS: 5
jobs:
deploy:
runs-on: ubuntu-latest
@@ -24,32 +28,84 @@ jobs:
- name: Install dependencies
run: |
cd oauth
cd ${{ env.OAUTH_DIR }}
npm install
- name: Build OAuth app
run: |
cd oauth
npm run build
cd ${{ env.OAUTH_DIR }}
NODE_ENV=production npm run build
- name: Copy OAuth build to static
run: |
mkdir -p my-blog/static/assets
cp -r oauth/dist/assets/* my-blog/static/assets/
cp oauth/dist/index.html my-blog/static/oauth/index.html || true
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
rm -rf my-blog/static/assets
cp -rf ${{ env.OAUTH_DIR }}/dist/* my-blog/static/
cp ${{ env.OAUTH_DIR }}/dist/index.html my-blog/templates/oauth-assets.html
- name: Build ailog
run: cargo build --release
- name: Build PDS app
run: |
cd pds
npm install
npm run build
- name: Copy PDS build to static
run: |
rm -rf my-blog/static/pds
cp -rf pds/dist my-blog/static/pds
- name: Cache ailog binary
uses: actions/cache@v4
with:
path: ./bin
key: ailog-bin-${{ runner.os }}
restore-keys: |
ailog-bin-${{ runner.os }}
- name: Setup ailog binary
run: |
# Get expected version from Cargo.toml
EXPECTED_VERSION=$(grep '^version' Cargo.toml | cut -d'"' -f2)
echo "Expected version from Cargo.toml: $EXPECTED_VERSION"
# Check current binary version if exists
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Current binary version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Check OS
OS="${{ runner.os }}"
echo "Runner OS: $OS"
# Use pre-packaged binary if version matches or extract from tar.gz
if [ "$CURRENT_VERSION" = "$EXPECTED_VERSION" ]; then
echo "Binary is up to date"
chmod +x ./bin/ailog
elif [ "$OS" = "Linux" ] && [ -f "./bin/ailog-linux-x86_64.tar.gz" ]; then
echo "Extracting ailog from pre-packaged tar.gz..."
cd bin
tar -xzf ailog-linux-x86_64.tar.gz
chmod +x ailog
cd ..
# Verify extracted version
EXTRACTED_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Extracted binary version: $EXTRACTED_VERSION"
if [ "$EXTRACTED_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Warning: Binary version mismatch. Expected $EXPECTED_VERSION but got $EXTRACTED_VERSION"
fi
else
echo "Error: No suitable binary found for OS: $OS"
exit 1
fi
- name: Build site with ailog
run: |
cd my-blog
../target/release/ailog build
../bin/ailog build
- name: List public directory
run: |
@@ -64,3 +120,50 @@ jobs:
directory: my-blog/public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
wranglerVersion: '3'
cleanup:
needs: deploy
runs-on: ubuntu-latest
if: success()
steps:
- name: Cleanup old deployments
run: |
curl -X PATCH \
"https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{ \"deployment_configs\": { \"production\": { \"deployment_retention\": ${{ env.KEEP_DEPLOYMENTS }} } } }"
# Get all deployments
DEPLOYMENTS=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json")
# Extract deployment IDs (skip the latest N deployments)
DEPLOYMENT_IDS=$(echo "$DEPLOYMENTS" | jq -r ".result | sort_by(.created_on) | reverse | .[${{ env.KEEP_DEPLOYMENTS }}:] | .[].id // empty")
if [ -z "$DEPLOYMENT_IDS" ]; then
echo "No old deployments to delete"
exit 0
fi
# Delete old deployments
for ID in $DEPLOYMENT_IDS; do
echo "Deleting deployment: $ID"
RESPONSE=$(curl -s -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments/$ID" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json")
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" = "true" ]; then
echo "Successfully deleted deployment: $ID"
else
echo "Failed to delete deployment: $ID"
echo "$RESPONSE" | jq .
fi
sleep 1 # Rate limiting
done
echo "Cleanup completed!"

View File

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

169
.github/workflows/release.yml vendored Normal file
View 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 }}

17
.gitignore vendored
View File

@@ -5,8 +5,23 @@
*.swo
*~
.DS_Store
cloudflare-config.yml
my-blog/public/
dist
node_modules
package-lock.json
my-blog/static/assets/comment-atproto-*
my-blog/static/ai-assets/comment-atproto-*
bin/ailog
docs
my-blog/static/index.html
my-blog/templates/oauth-assets.html
cloudflared-config.yml
.config
repos
oauth_old
oauth_example
my-blog/static/oauth/assets/comment-atproto*
*.lock
my-blog/config.toml
.claude/settings.local.json
my-blog/static/pds

View File

@@ -1,6 +1,6 @@
[package]
name = "ailog"
version = "0.1.0"
version = "0.3.1"
edition = "2021"
authors = ["syui"]
description = "A static blog generator with AI features"
@@ -10,12 +10,16 @@ license = "MIT"
name = "ailog"
path = "src/main.rs"
[lib]
name = "ailog"
path = "src/lib.rs"
[dependencies]
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 +30,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"
@@ -35,6 +39,8 @@ urlencoding = "2.1"
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.5", features = ["cors", "fs"] }
axum-extra = { version = "0.9", features = ["typed-header"] }
tracing = "0.1"
hyper = { version = "1.0", features = ["full"] }
tower-sessions = "0.12"
jsonwebtoken = "9.2"
@@ -43,12 +49,41 @@ 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 }
rpassword = "7.3"
rustyline = "14.0"
dirs = "5.0"
[dev-dependencies]
tempfile = "3.14"
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

View File

@@ -1,32 +0,0 @@
# Multi-stage build for ailog
FROM rust:1.75 as builder
WORKDIR /usr/src/app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy the binary
COPY --from=builder /usr/src/app/target/release/ailog /usr/local/bin/ailog
# Copy blog content
COPY my-blog ./blog
# Build static site
RUN ailog build blog
# Expose port
EXPOSE 8080
# Run server
CMD ["ailog", "serve", "blog"]

908
README.md

File diff suppressed because it is too large Load Diff

Binary file not shown.

222
claude.md
View File

@@ -1,5 +1,227 @@
# エコシステム統合設計書
## 注意事項
`console.log`は絶対に書かないようにしてください。
ハードコードしないようにしてください。必ず、`./my-blog/config.toml``./oauth/.env.production`を使用するように。または`~/.config/syui/ai/log/config.json`を使用するように。
重複する名前のenvを作らないようにしてください。新しい環境変数を作る際は必ず検討してください。
```sh
# ダメな例
VITE_OAUTH_COLLECTION_USER=ai.syui.log.user
VITE_OAUTH_COLLECTION_CHAT=ai.syui.log.chat
```
## oauth appの設計
> ./oauth/.env.production
```sh
VITE_ATPROTO_PDS=syu.is
VITE_ADMIN_HANDLE=ai.syui.ai
VITE_AI_HANDLE=ai.syui.ai
VITE_OAUTH_COLLECTION=ai.syui.log
```
これらは非常にシンプルな流れになっており、すべての項目は、共通します。短縮できる場合があります。handleは変わる可能性があるので、できる限りdidを使いましょう。
1. handleからpds, didを取得できる ... com.atproto.repo.describeRepo
2. pdsが分かれば、pdsApi, bskyApi, plcApiを割り当てられる
3. bskyApiが分かれば、getProfileでavatar-uriを取得できる ... app.bsky.actor.getProfile
4. pdsAPiからアカウントにあるcollectionのrecordの情報を取得できる ... com.atproto.repo.listRecords
### コメントを表示する
1. VITE_ADMIN_HANDLEから管理者のhandleを取得する。
2. VITE_ATPROTO_PDSから管理者のアカウントのpdsを取得する。
3. pdsからpdsApi, bskApi, plcApiを割り当てる。
```rust
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => {
// Default to Bluesky network for unknown PDS
NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
}
```
4. 管理者アカウントであるVITE_ADMIN_HANDLEとVITE_ATPROTO_PDSから`ai.syui.log.user`というuserlistを取得する。
```sh
curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${VITE_ADMIN_HANDLE}&collection=ai.syui.log.user"
---
syui.ai
```
5. ユーザーがわかったら、そのユーザーのpdsを判定する。
```sh
curl -sL "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=syui.ai" |jq -r ".didDoc.service.[].serviceEndpoint"
---
https://shiitake.us-east.host.bsky.network
curl -sL "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=syui.ai" |jq -r ".did"
---
did:plc:uqzpqmrjnptsxezjx4xuh2mn
```
6. pdsからpdsApi, bskApi, plcApiを割り当てる。
```rust
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => {
// Default to Bluesky network for unknown PDS
NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
}
```
7. ユーザーの情報を取得、表示する
```sh
bsky_api=https://public.api.bsky.app
user_did=did:plc:uqzpqmrjnptsxezjx4xuh2mn
curl -sL "$bsky_api/xrpc/app.bsky.actor.getProfile?actor=$user_did"|jq -r .avatar
---
https://cdn.bsky.app/img/avatar/plain/did:plc:uqzpqmrjnptsxezjx4xuh2mn/bafkreid6kcc5pnn4b3ar7mj6vi3eiawhxgkcrw3edgbqeacyrlnlcoetea@jpeg
```
### AIの情報を表示する
AIが持つ`ai.syui.log.chat.lang`, `ai.syui.log.chat.comment`を表示します。
なお、これは通常、`VITE_ADMIN_HANDLE`にputRecordされます。そこから情報を読み込みます。`VITE_AI_HANDLE`はそのrecordの`author`のところに入ります。
```json
"author": {
"did": "did:plc:4hqjfn7m6n5hno3doamuhgef",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hqjfn7m6n5hno3doamuhgef/bafkreiaxkv624mffw3cfyi67ufxtwuwsy2mjw2ygezsvtd44ycbgkfdo2a@jpeg",
"handle": "yui.syui.ai",
"displayName": "ai"
}
```
1. VITE_ADMIN_HANDLEから管理者のhandleを取得する。
2. VITE_ATPROTO_PDSから管理者のアカウントのpdsを取得する。
3. pdsからpdsApi, bskApi, plcApiを割り当てる。
```rust
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => {
// Default to Bluesky network for unknown PDS
NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
}
```
4. 管理者アカウントであるVITE_ADMIN_HANDLEとVITE_ATPROTO_PDSから`ai.syui.log.chat.lang`, `ai.syui.log.chat.comment`を取得する。
```sh
curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${VITE_ADMIN_HANDLE}&collection=ai.syui.log.chat.comment"
```
5. AIのprofileを取得する。
```sh
curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.describeRepo?repo=$VITE_AI_HANDLE" |jq -r ".didDoc.service.[].serviceEndpoint"
---
https://syu.is
curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.describeRepo?repo=$VITE_AI_HANDLE" |jq -r ".did"
did:plc:6qyecktefllvenje24fcxnie
```
6. pdsからpdsApi, bskApi, plcApiを割り当てる。
```rust
match pds {
"bsky.social" | "bsky.app" => NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
},
"syu.is" => NetworkConfig {
pds_api: "https://syu.is".to_string(),
plc_api: "https://plc.syu.is".to_string(),
bsky_api: "https://bsky.syu.is".to_string(),
web_url: "https://web.syu.is".to_string(),
},
_ => {
// Default to Bluesky network for unknown PDS
NetworkConfig {
pds_api: format!("https://{}", pds),
plc_api: "https://plc.directory".to_string(),
bsky_api: "https://public.api.bsky.app".to_string(),
web_url: "https://bsky.app".to_string(),
}
}
}
```
7. AIの情報を取得、表示する
```sh
bsky_api=https://bsky.syu.is
user_did=did:plc:6qyecktefllvenje24fcxnie
curl -sL "$bsky_api/xrpc/app.bsky.actor.getProfile?actor=$user_did"|jq -r .avatar
---
https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg
```
## 中核思想
- **存在子理論**: この世界で最も小さいもの(存在子/aiの探求
- **唯一性原則**: 現実の個人の唯一性をすべてのシステムで担保

View File

@@ -1,18 +0,0 @@
tunnel: ec5a422d-7678-4e73-bf38-6105ffd4766a
credentials-file: /Users/syui/.cloudflared/ec5a422d-7678-4e73-bf38-6105ffd4766a.json
ingress:
- hostname: log.syui.ai
service: http://localhost:4173
originRequest:
noHappyEyeballs: true
- hostname: ollama.syui.ai
service: http://localhost:11434
originRequest:
noHappyEyeballs: true
httpHostHeader: "localhost:11434"
# Cloudflare Accessを無効化する場合は以下をコメントアウト
# accessPolicy: bypass
- service: http_status:404

View File

@@ -7,6 +7,7 @@ author = "syui"
[build]
highlight_code = true
highlight_theme = "Monokai"
minify = false
[ai]
@@ -15,15 +16,108 @@ auto_translate = false
comment_moderation = false
ask_ai = true
provider = "ollama"
model = "gemma3:2b"
host = "https://ollama.syui.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."
ai_did = "did:plc:4hqjfn7m6n5hno3doamuhgef"
model = "gemma3"
host = "localhost:11434"
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
handle = "ai.syui.ai"
[ai.profiles]
[ai.profiles.user]
did = "did:plc:vzsvtbtbnwn22xjqhcu3vd6y"
handle = "syui.syui.ai"
display_name = "syui"
avatar_url = "https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreif62mqyra4ndv6ohlscl7adp3vhalcjxwhs676ktfj2sq2drs3pdi@jpeg"
profile_url = "https://syu.is/profile/did:plc:vzsvtbtbnwn22xjqhcu3vd6y"
[ai.profiles.ai]
did = "did:plc:6qyecktefllvenje24fcxnie"
handle = "ai.syui.ai"
display_name = "ai"
avatar_url = "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreigo3ucp32carhbn3chfc3hlf6i7f4rplojc76iylihzpifyexi24y@jpeg"
profile_url = "https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie"
[ai.templates]
fallback = """なるほど!面白い話題だね!
{question}
アイが思うに、この手の技術って急速に進歩してるから、具体的な製品名とか実例を交えて話した方が分かりやすいかもしれないの!
最近だと、AI関連のツールやプロトコルがかなり充実してきてて、実用レベルのものが増えてるんだよ
アイは宇宙とかAIとか、難しい話も知ってるから、特にどんな角度から深掘りしたいの実装面それとも将来的な可能性とかアイと一緒に考えよう"""
[[ai.templates.responses]]
keywords = ["ゲーム", "game", "npc", "NPC"]
priority = 1
template = """わあゲームの話だねアイ、ゲームのAIってすっごく面白いと思う
{question}
アイが知ってることだと、最近のゲームはNPCがお話できるようになってるんだって**Inworld AI**っていうのがUE5で使えるようになってるし、**Unity Muse**も{current_year}年から本格的に始まってるんだよ!
アイが特に面白いと思うのは、**MCP**っていうのを使うと:
- GitHub MCPでゲームのファイル管理ができる
- Weather MCPでリアルタイムのお天気が連動する
- Slack MCPでチーム開発が効率化される
スタンフォードの研究では、ChatGPTベースのAI住民が自分で街を作って生活してるのを見たことがあるの数年後にはNPCの概念が根本的に変わりそうで、わくわくしちゃう
UE5への統合、どんな機能から試したいのアイも一緒に考えたい"""
[[ai.templates.responses]]
keywords = ["AI", "ai", "MCP", "mcp"]
priority = 1
template = """AIとMCPの話アイの得意分野だよ
{question}
{current_year}年の状況だと、MCP市場が拡大してて、実用的なサーバーが数多く使えるようになってるの
アイが知ってる開発系では:
- **GitHub MCP**: PR作成とリポジトリ管理が自動化
- **Docker MCP**: コンテナ操作をAIが代行
- **PostgreSQL MCP**: データベース設計・最適化を支援
クリエイティブ系では:
- **Blender MCP**: 3Dモデリングの自動化
- **Figma MCP**: デザインからコード変換
**Zapier MCP**なんて数千のアプリと連携できるから、もう手作業でやってる場合じゃないよね!
アイは小さい物質のことも知ってるから、どの分野でのMCP活用を考えてるのか教えて具体的なユースケースがあると、もっと詳しくお話できるよ"""
[oauth]
json = "client-metadata.json"
redirect = "oauth/callback"
admin = "did:plc:uqzpqmrjnptsxezjx4xuh2mn"
collection_comment = "ai.syui.log"
collection_user = "ai.syui.log.user"
collection_chat = "ai.syui.log.chat"
admin = "ai.syui.ai"
collection = "ai.syui.log"
pds = "syu.is"
handle_list = ["syui.syui.ai", "ai.syui.ai", "ai.ai"]
[blog]
base_url = "https://syui.ai"
content_dir = "./my-blog/content/posts"
[profiles]
[profiles.user]
handle = "syui.syui.ai"
did = "did:plc:vzsvtbtbnwn22xjqhcu3vd6y"
display_name = "syui"
avatar_url = "https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreif62mqyra4ndv6ohlscl7adp3vhalcjxwhs676ktfj2sq2drs3pdi@jpeg"
profile_url = "https://syu.is/profile/did:plc:vzsvtbtbnwn22xjqhcu3vd6y"
[profiles.ai]
handle = "ai.syui.ai"
did = "did:plc:6qyecktefllvenje24fcxnie"
display_name = "ai"
avatar_url = "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreigo3ucp32carhbn3chfc3hlf6i7f4rplojc76iylihzpifyexi24y@jpeg"
profile_url = "https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie"
[paths]
claude_paths = [
"/Users/syui/.claude/local/claude",
"claude",
"/usr/local/bin/claude",
"/opt/homebrew/bin/claude"
]

View File

@@ -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
@@ -21,7 +17,7 @@ $ git clone https://git.syui.ai/ai/log
$ cd log
$ cargo build
$ ./target/debug/ailog init my-blog
$ ./target/debug/ailog server my-blog
$ ./target/debug/ailog serve my-blog
```
## install
@@ -34,7 +30,7 @@ $ export RUSTUP_HOME="$HOME/.rustup"
$ export PATH="$HOME/.cargo/bin:$PATH"
---
$ which ailog
$ ailog
$ ailog -h
```
## build deploy
@@ -61,27 +57,37 @@ $ npm run build
$ npm run preview
```
```sh
```sh:ouath/.env.production
# Production environment variables
VITE_APP_HOST=https://example.com
VITE_OAUTH_CLIENT_ID=https://example.com/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://example.com/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
VITE_COLLECTION_COMMENT=ai.syui.log
VITE_COLLECTION_USER=ai.syui.log.user
VITE_COLLECTION_CHAT=ai.syui.log.chat
# Base collection (all others are derived via getCollectionNames)
VITE_OAUTH_COLLECTION=ai.syui.log
# Collection names for ailog (backward compatibility)
AILOG_COLLECTION_COMMENT=ai.syui.log
AILOG_COLLECTION_USER=ai.syui.log.user
# AI Configuration
VITE_AI_ENABLED=true
VITE_AI_ASK_AI=true
VITE_AI_PROVIDER=ollama
VITE_AI_MODEL=gemma3:4b
VITE_AI_HOST=https://ollama.syui.ai
VITE_AI_SYSTEM_PROMPT="ai"
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
# API Configuration
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
VITE_ATPROTO_API=https://bsky.social
```
### 解説
これは`ailog oauth build my-blog`で`./my-blog/config.toml`から`./oauth/.env.production`が生成されます。
```sh
$ ailog oauth build my-blog
```
### use
簡単に説明すると、`./oauth`で生成するのが`atproto-comment-system`です。
@@ -113,15 +119,8 @@ $ cloudflared tunnel --config cloudflared-config.yml run
$ cloudflared tunnel route dns ${uuid} example.com
```
以下の2つのcollection recordを生成します。ユーザーには`ai.syui.log`が生成され、ここにコメントが記録されます。それを取得して表示しています。`ai.syui.log.user`は管理者である`VITE_ADMIN_DID`用です。
```sh
VITE_COLLECTION_COMMENT=ai.syui.log
VITE_COLLECTION_USER=ai.syui.log.user
```
```sh
$ ailog auth login
$ ailog auth init
$ ailog stream server
```
@@ -133,5 +132,44 @@ $ ailog stream server
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
local llm, mcp, atproto組み合わせです。
`llm`, `mcp`, `atproto`などの組み合わせです。
現在、`/index.json`を監視して、更新があれば、翻訳などを行い自動ポストする機能があります。
## 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!");
```
## msg
[msg type="info" content="これは情報メッセージです。重要な情報を読者に伝えるために使用します。"]
{{< msg type="warning" content="これは警告メッセージです。注意が必要な情報を示します。" >}}
[msg type="error" content="これはエラーメッセージです。問題やエラーを示します。"]
{{< msg type="success" content="これは成功メッセージです。操作が成功したことを示します。" >}}
[msg type="note" content="これはノートメッセージです。補足情報や備考を示します。"]
[msg content="これはデフォルトメッセージです。タイプが指定されていない場合、自動的に情報メッセージとして表示されます。"]
## img-compare
[img-compare before="/img/ue_blender_model_ai_v0401.png" after="/img/ue_blender_model_ai_v0501.png" width="800" height="300"]

View File

@@ -0,0 +1,66 @@
---
title: "ブログを移行した"
slug: "blog"
date: 2025-06-14
tags: ["blog", "cloudflare", "github"]
draft: false
---
ブログを移行しました。過去のブログは[syui.github.io](https://syui.github.io)にありあます。
1. `gh-pages`から`cf-pages`への移行になります。
2. 自作の`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'
```
## url
- [https://syui.pages.dev](https://syui.pages.dev)
- [https://syui.github.io](https://syui.github.io)

View File

@@ -0,0 +1,78 @@
---
title: "oauthに対応した"
slug: "oauth"
date: 2025-06-19
tags: ["atproto"]
draft: false
---
現在、[syu.is](https://syu.is)に[atproto](https://github.com/bluesky-social/atproto)をselfhostしています。
oauthを`bsky.social`, `syu.is`ともに動くようにしました。
![](/img/atproto_oauth_syuis.png)
ここでいうselfhostは、pds, plc, bsky, bgsなどを自前のserverで動かし、連携することをいいいます。
ちなみに、atprotoは[bluesky](https://bsky.app)のようなものです。
ただし、その内容は結構複雑で、`at://did`の仕組みで動くsnsです。
usernameは`handle`という`domain`の形を採用しています。
didの名前解決(dns)をしているのが`plc`です。`pds`はuserのdataを保存しています。timelineに配信したり表示しているのが`bsky(appview)`, 統合しているのが`bgs`です。
その他、`social-app`がclientで、`ozone`がmoderationです。
```sh
"6qyecktefllvenje24fcxnie" -> "ai.syu.is"
```
## oauthでハマったところ
現在、`bsky.team`のpds, plc, bskyには`did:plc:6qyecktefllvenje24fcxnie`が登録されています。これは`syu.is``@ai.syui.ai`のアカウントです。
```sh
$ did=did:plc:6qyecktefllvenje24fcxnie
$ curl -sL https://plc.syu.is/$did|jq .alsoKnownAs
[ "at://ai.syui.ai" ]
$ curl -sL https://plc.directory/$did|jq .alsoKnownAs
[ "at://ai.syu.is" ]
```
しかし、みて分かる通り、bskyではhandle-changeが反映されていますが、pds, plcは`@ai.syu.is`で登録されており、更新されていないようです。
```sh
$ handle=ai.syui.ai
$ curl -sL "https://syu.is/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
did:plc:6qyecktefllvenje24fcxnie
$ curl -sL "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
null
$ curl -sL "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
did:plc:6qyecktefllvenje24fcxnie
```
[msg type="warning" content="現在はbsky.teamのpdsにhandle-changeが反映されています。"]
oauthは、そのままではbsky.teamのpds, plcを使って名前解決を行います。この場合、まず、それらのserverにdidが登録されている必要があります。
次に、handleの更新が反映されている必要があります。もし反映されていない場合、handleとpasswordが一致しません。
localhostではhandleをdidにすることで突破できそうでしたが、本番環境では難しそうでした。
なお、[@atproto/oauth-provider](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-provider)の本体を書き換えて、pdsで使うと回避は可能だと思います。
私の場合は、その方法は使わず、didの名前解決には自前のpds, plcを使用することにしました。
```js
this.oauthClient = await BrowserOAuthClient.load({
clientId: this.getClientId(),
handleResolver: pdsUrl,
plcDirectoryUrl: pdsUrl === 'https://syu.is' ? 'https://plc.syu.is' : 'https://plc.directory',
});
```

View File

@@ -0,0 +1,40 @@
---
title: "world system v0.2"
slug: "ue"
date: 2025-06-30
tags: ["ue", "blender"]
draft: false
---
最近のゲーム開発の進捗です。
## world system
現在、ue5.6で新しく世界を作り直しています。
これは、ゲーム開発のproject内でworld systemという名前をつけた惑星形式のmapを目指す領域になります。
現在、worldscape + udsで理想に近い形のmapができました。ただ、問題もたくさんあり、重力システムと天候システムです。
```sh
[issue]
1. 天候システム
2. 重力システム
```
ですが、今までのworld systemは、大気圏から宇宙に移行する場面や陸地が存在しない点、地平線が不完全な点などがありましたが、それらの問題はすべて解消されました。
```sh
[update]
1. 大気圏から宇宙に移行する場面が完全になった
2. 陸地ができた
3. 地平線が完全なアーチを描けるように
4. 月、惑星への着陸ができるようになった
5. 横から惑星に突入できるようになった
```
面白い動画ではありませんが、現状を記録しておきます。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/K0solfQAQTQ?si=B6qD-WUODTUpWZ0y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -0,0 +1,80 @@
---
title: "aiosを作り直した"
slug: "aios"
date: 2025-07-05
tags: ["os"]
draft: false
---
`aios`とは自作osのことで、archlinuxをベースにしていました。
```sh
#!/bin/zsh
git clone https://gitlab.archlinux.org/archlinux/archiso
cp -rf ./cfg/profiledef.sh ./archiso/configs/releng/profiledef.sh
cp -rf ./cfg/profiledef.sh ./archiso/configs/baseline/profiledef.sh
cp -rf ./scpt/mkarchiso ./archiso/archiso/mkarchiso
./archiso/archiso/mkarchiso -v -o ./ ./archiso/configs/releng/
tar xf aios-bootstrap*.tar.gz
mkdir -p root.x86_64/var/lib/machines/arch
pacstrap -c root.x86_64/var/lib/machines/arch base
echo -e 'Server = http://mirrors.cat.net/archlinux/$repo/os/$arch
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' >> ./root.x86_64/etc/pacman.d/mirrorlist
sed -i s/CheckSpace/#CheckeSpace/ root.x86_64/etc/pacman.conf
arch-chroot root.x86_64 /bin/sh -c 'pacman-key --init'
arch-chroot root.x86_64 /bin/sh -c 'pacman-key --populate archlinux'
arch-chroot root.x86_64 /bin/sh -c 'pacman -Syu --noconfirm base base-devel linux'
tar -zcvf aios-bootstrap.tar.gz root.x86_64/
```
```sh:./cfg/profiledef.sh
#!/usr/bin/env bash
# shellcheck disable=SC2034
iso_name="aios"
iso_label="AI_$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y%m)"
iso_publisher="ai os <https://git.syui.ai/ai/os>"
iso_application="ai os Live/Rescue DVD"
iso_version="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)"
install_dir="ai"
#buildmodes=('iso')
buildmodes=('bootstrap')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-ia32.grub.esp' 'uefi-x64.grub.esp'
'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito')
arch="x86_64"
pacman_conf="pacman.conf"
airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M')
file_permissions=(
["/etc/shadow"]="0:0:400"
["/root"]="0:0:750"
["/root/.automated_script.sh"]="0:0:755"
["/root/.gnupg"]="0:0:700"
["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/Installation_guide"]="0:0:755"
["/usr/local/bin/livecd-sound"]="0:0:755"
)
```
## rust + unix
一からosを作りたいと思っていたので、rustでunixのosを作り始めました。
![](/img/aios_v0201.png)
名前は`Aios`にして、今回は`syui`のprojectとして作り始めました。
後に`ai/os`と統合するかもしれません。
1. [https://git.syui.ai/ai/os](https://git.syui.ai/ai/os)
```sh
#!/bin/zsh
d=${0:a:h:h}
cd $d/kernel
cargo bootimage --release
BOOT_IMAGE="../target/x86_64-unknown-none/release/bootimage-aios-kernel.bin"
qemu-system-x86_64 -drive format=raw,file="$BOOT_IMAGE"
```

View File

@@ -0,0 +1,114 @@
---
title: "yui system v0.2.1"
slug: "blender"
date: 2025-07-11
tags: ["blender", "ue", "vmc"]
draft: false
---
`yui system`をupdateしました。別名、`unique system`ともいい、プレイヤーの唯一性を担保するためのもので、キャラクターのモデルもここで管理します。
今回は、blenderでモデルを作り直している話になります。
## blenderで作るvrm
モデルをblenderで作り直すことにしました。
vroidからblenderに移行。blenderでmodelを作る作業はとても大変でした。
今回は、素体と衣装を別々に作り組み合わせています。完成度の高いモデルをいくつか参考にしています。
materialも分離したため、ue5で指定しやすくなりました。これによって変身時にue5のmaterialを指定しています。eyeのmaterialを分離して色を付けています。
![](/img/ue_blender_model_ai_v0604.png)
## modelの変遷
[img-compare before="/img/ue_blender_model_ai_v0601.png" after="/img/ue_blender_model_ai_v0602.png" width="800" height="300"]
[msg type="info" content="v0.1: vroidからblenderへ移行。blenderは初めてなので簡単なことだけ実行。"]
[img-compare before="/img/ue_blender_model_ai_v0602.png" after="/img/ue_blender_model_ai_v0603.png" width="800" height="300"]
[msg type="info" content="v0.2: blenderの使い方を次の段階へシフト。最初から作り直す。様々な問題が発生したが、大部分を解消した。"]
しかし、まだまだ問題があり、細かな調整が必要です。
[msg type="error" content="衣装同士、あるいは体が多少すり抜ける事がある。ウェイトペイントやボーンの調整が完璧ではない。"]
## eyeが動かない問題を解決
`vmc`で目玉であるeyeだけ動かないことに気づいて修正しました。
`eye`の部分だけvroid(vrm)のboneを使うことで解決できました。しかし、新たにblenderかvrm-addonのbugに遭遇しました。具体的にはboneがxyz軸で動かせなくなるbugです。これは不定期で発生していました。boneを動かせるときと動かせなくなるときがあり、ファイルは同じものを使用。また、スクリプト画面ではboneを動かせます。
## 指先がうまく動かない問題を解決
vmcで指先の動きがおかしくなるので、ウェイトペイントを塗り直すと治りました。
## worldscapeで足が浮いてしまう問題を解決
worldscapeでは陸地に降り立つとプレイヤーが浮いてしまいます。
gaspのabpでfoot placementを外す必要がありました。これは、モデルの問題ではなく、gaspのキャラクターすべてで発生します。
ここの処理を削除します。
<iframe src="https://blueprintue.com/render/wrrxz9vm" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
## 衣装のガビガビを解決
昔からあった衣装のガビガビは重複する面を削除することで解消できました。
```md
全選択A キー)
Mesh → Clean Up → Merge by Distance
距離を0.000にして実行
```
## materialの裏表を解決
これはue5で解消したほうがいいでしょう。編集していると、面の裏表の管理が面倒なことがあります。
materialで`Two Sided`を有効にします。
## キャラクターのエフェクトを改良
これらの処理を簡略化できました。最初は雑に書いていましたが、vrmは何度も修正し、上書きされますから、例えば、`SK_Mesh`でmaterialを設定する方法はよくありません。
<iframe src="https://blueprintue.com/render/gue0vayu" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
## gameplay camera pluginをue5.6に対応
ue5.5と5.6では関数も他の処理も変わっていて、rotationを`BP_Player`でsetすると、crashするbugがあります。
基本的には、`Blueprints/Cameras/CameraRigPrefab_BasicThiredPersonBehavior`をみてください。
<iframe src="https://blueprintue.com/render/-e0r7oxq" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
![](https://git.syui.ai/attachments/019d2079-1450-4271-8816-ded92f60b3c9)
キャラクターが動く場合は、`Update Rotation Pre CMC`にある`Use Controller Desired Rotation`, `Orient Rotation To Movement`の処理です。両方を`true`にしましょう。
`vmc`時もこれで対処します。
## gaspでidle, sprintをオリジナルに変更
これはabpで設定します。設定方法はue5.5と変わりません。
[https://ue-book.syui.ai/gasp/11_run.html](https://ue-book.syui.ai/gasp/11_run.html)
## vrm4uのvmcに対応
まず、clientはwabcam motion captureが最も自然に動作しています。
[msg type="warning" content="これは1年くらい前の検証結果です。現在はもっとよいvmc clientの選択肢があるかもしれません。"]
次に、`ABP_Pose_$NAME`が作られますが、vrmはよく更新しますので、`SK_Mesh`でcustom ABPを指定すると楽でしょう。
![](https://git.syui.ai/attachments/758407eb-5e77-4876-830b-ba4a78884e8d)
## youtube
<iframe width="100%" height="420" src="https://www.youtube.com/embed/qggHtmkMIko?vq=hd1080&rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -0,0 +1,36 @@
---
title: "yui system v0.2.2"
slug: "blender2"
date: 2025-07-11
tags: ["blender", "ue", "vmc"]
draft: false
---
新しい問題を発見したので、それらを解消しました。
## wingがbodyに入り込んでしまう
wingとmodelは分離させています。衣装の着せ替えを簡単にできるようにすること。それが新しく作ったblender modelの方針でした。
ただ、調整が難しくなったのも事実で、例えば、colliderの調整ができません。これによってbodyに入り込んでしまうことが多くなりました。
これは、とりあえず、wingのcolliderやboneを追加すること、そして、modelのneckに変更することで解消しました。
ただし、この方法も完璧ではないかもしれません。
## vmcではwingが追従しない
modelと分離しているので、vmc時には追従しません。したがって、wingのabpでmodelと同じvmcを入れます。これで解消できました。
## vrmでcustom abpを使用するとueがcrashする
vrm4uで`.vrm`をimportすると`SK_$NAME`にcustom abpを設定していた場合はueがcrashします。
上書きimportするならこれをnone(clear)に変更します。
## modelの頭身を調整
比較画像を出した際に、少しmodelのバランスが悪かったので調整しました。
具体的には、髪の毛を少し下げました。

View File

@@ -0,0 +1,155 @@
---
title: "自作ゲームのsystemを説明する"
slug: "game"
date: 2025-07-12
tags: ["ue"]
draft: false
---
現在、自作ゲームを開発しています。
このゲームには4つの柱があり、それらはsystemで分けられています。そして、systemは根本的な2つの価値観に基づきます。
根本的な2つの価値観は、(1)現実を反映すること、(2)この世界に同じものは一つもないという唯一性になります。
1. 現実の反映
2. 唯一性の担保
では、各systemについて説明していきます。
# system
## world system
別名、planet systemといいます。
現実の反映という価値観から、ゲーム世界もできる限り現実に合わせようと思いworld systemを作っています。
ゲームは通常、平面世界です。これはゲームエンジンのルールであり、基本的にゲーム世界は平面をベースにしています。
ですから、例えば、上に行っても、下に行っても、あるいは右に行っても、左に行っても、ずっと地平線が広がっています。
しかし、現実世界では、上に行けば、やがて大気圏を越え、宇宙に出ます。
最初は昔から認知されていた地球、月、太陽という3つの星を現実に合わせて作りました。
そして、マップをできる限り惑星形式にします。
これは非常に難しいことで、現在もいくつか問題を抱えています。
ただし、このworld systemの問題がゲームプレイに影響するかと言われると、殆どの場合、影響しません。ゲームプレイの領域は、最初は非常に狭い範囲で作ろうと思っています。小さなところから完璧に作っていきたいという思いがあります。
つまり、プレイヤーは空にも宇宙にも到達できません。それが見えるかどうかもわかりません。しかし、見えない部分もしっかりと作り、世界があるということが私にとって大切です。
まずは、狭いけど完璧な空間を作り、そこでゲームシステムを完成させます。広い世界はできる限り見えないようにしたほうがいいでしょう。夢の世界のような狭い空間を作り、そこでシンプルで小さいゲームができます。もちろん、広い世界に出ることはできません。そもそもこのゲーム、見えない部分をちゃんと作る、そこにも世界がちゃんとあるというのをテーマにしているので、広い世界で何かをやるようなゲームを目指していなかったりします。なにかのときに垣間見える、かもしれない外の世界、広い世界。それを感じられることがある、ということ。それが重要なので、このsystem自体は背景に過ぎないのです。
最初から広い世界があるのではなく`狭い世界 -> 広い世界`への移行が重要だと考えています。この移行に関しては、演出というテーマに基づき、設計する必要があります。それがゲームとしての面白さを作る、ということなのだと思います。
## yui system
別名、unique systemといいます。プレイヤーの唯一性を担保するためのsystemです。
とはいえ、色々なものがここに詰め込まれるでしょう。characterのモデリングとかもそうですね。
どのように担保していくかは未定ですが、いくつか案があります。配信との連携、vmcでモーションキャプチャなどを考えていました。
## ai system
別名、ability systemといいます。
主に、ゲーム性に関することです。ゲーム性とはなにか。それは、永続するということです。
例えば、将棋やオセロを考えてみてください。無限の組み合わせがあり、可能であればずっと遊んでいられる。そのような仕組みを目指します。
まずは属性を物語から考えます。物語は最も小さい物質の探求です。アクシオンやバリオンなどの架空の物質、そして、中性子や原子などの現実の物質が属性となり、1キャラクターにつき1属性を持ちます。
## at system
別名、account systemといいます。
プレイヤーが現実のアカウントを使用してプレイできることを目指します。`atproto`を採用して、ゲームデータを個人のアカウントが所有することを目指しています。
# 現実の反映とはなにか
わかり易い言葉で「現実の反映」を目指すと言いましたが、これはどういうことでしょう。
私の中では「同一性」とも言い換えられます。
例えば、現実の世界とゲームの世界があるのではなく「すべてが現実である」という考え方をします。言い換えると「すべて同じもの」ということ。
もし多くの人が現実世界とゲーム世界を別物と捉えているなら、できる限りその認識を壊す方向で考えます。
例えば、`at system`では現実のsnsアカウントをゲームアカウントに使用したり、現実の出来事をゲームに反映したり、またはゲームの出来事を現実に反映する仕組みを考えます。
全ては一つ、一つはすべて。
同一性と唯一性は一見して矛盾しますが、その統合を考えます。
# 物語と実装
```md
# 物語-存在
同一性
唯一性
# system-実装
world system
yui system
ai system
at system
```
物語では、この世界のものは全て存在であると説きます。存在しかない世界。存在だけがある世界。そして、あらゆる存在を構築しているこの世界で最も小さいものが「存在子」です。存在子は別名、アイといいます。そして、このアイにも同じものはありません。すべての存在子は異なるもの、別の意識。
アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。
> アイは、この世界と一緒だからね。同じものは一つもないよ。
# どこまで実装できた
実は、上記のsystemは既にすべてを実装したことがあります。
```md
[at system]
ゲームが始まると、atprotoのaccountでloginでき、取得したアイテムなどはatproto(pds)に保存されます。
[ai system]
キャラクターは属性攻撃ができます。
[world system]
上へ上へと飛んでいけば、雲を超え、宇宙空間に出られます。
[yui system]
配信環境やvmcでキャラクターを動かすことができます。
```
しかし、ue5.5で作っていたsystemも、ue5.6にupdateすると全て動かなくなりました。また一から作り直しています。私は、モデルの作り方から、ゲームの作り方まで初心者ですから、何度も作り直すことで、ゲーム作りを覚えられます。
そして、まだ革新的なアイディアを見つけられていません。それはシンプルで身近にあり、人々が面白いと思うもの。まだゲームになっていない、あるいはあまり知られていないものである必要があります。
例えば、ウマ娘でいうと競馬、ポケモンでいうと捕獲、になります。
それを見つけ、ゲームに取り込む事ができれば完成と言えるでしょう。
そして、ゲームに取り込むことが複雑で難しすぎるようなものではありません。シンプルで単純でわかりやすいものでなければなりません。
## versionを付ける
そろそろversionを付けるかどうか迷っています。
今までモヤモヤしていたものが、最近はよりはっきりしてきたと感じます。ただ、versionはあまり覚えていないし、付ける意味もない。これまではそうでした。
もしかすると今もそうかもしれません。色々なものがバラバラで管理しきれないのです。
ですが、今までやってきたことを総合すると、現在は、`v0.2`くらいだと思います。
最初、はじめてueを触ったときに宇宙マップを使って構築しました。これをv0.0としましょう。
次に、city sampleと宇宙を統合しました。これがv0.1です。
最近はworldscapeを使ってマップを構築しています。これがv0.2です。
aiというキャラクターモデルの変遷も大体を3つの段階に分けられると思います。初めてモデルを作った、vroidで作ったのがv0.0、blenderを初めて触ったのがv0.1、現在がv0.2です。
とはいえ、この設定もそのうち忘れ、どこかで圧縮されてしまうかもしれませんが、覚えているならここから徐々にversionが上がっていくでしょう。

View File

@@ -0,0 +1,48 @@
---
title: "chromeからfirefoxに移行した"
slug: "firefox"
date: 2025-07-14
tags: ["chrome", "firefox", "browser"]
draft: false
---
AIから勧められたのでchromeからfirefoxに移行しました。
chromeにとどまっていた理由は、翻訳機能です。
しかし、firefoxにも翻訳機能betaが来ていて、日本語が翻訳できるようになっていました。
[https://support.mozilla.org/ja/kb/website-translation](https://support.mozilla.org/ja/kb/website-translation)
chromeからの移行理由は、主に[gorhill/ublock](https://github.com/gorhill/ublock)です。
## chromeを使い続ける方法
私はfirefoxに移行しましたが、いくつか回避策があります。
`chrome://flags`でいくつかの機能のenable, disableを切り替えます。
```json
{
"url": "chrome://flags",
"purpose": "Maintain Manifest V2 extension support",
"versions": {
"138": {
"enabled": [
"Temporarily unexpire M137 flags",
"Allow legacy extension manifest versions"
],
"disabled": [
"Extension Manifest V2 Deprecation Warning Stage",
"Extension Manifest V2 Deprecation Disabled Stage",
"Extension Manifest V2 Deprecation Unsupported Stage"
]
},
"139": {
"enabled": [
"Temporarily unexpired M138 flags"
]
}
}
}
```

View File

@@ -0,0 +1,10 @@
---
title: "ゲームとAI制御"
slug: "6bf4b020"
date: "2025-07-16"
tags: ["ai", "conversation"]
draft: false
extra:
type: "ai"
---

View File

@@ -0,0 +1,40 @@
---
title: "AIとの会話をブログにする"
slug: "ailog"
date: "2025-07-16"
tags: ["blog", "rust", "atproto"]
draft: false
---
今後、ブログはどのように書かれるようになるのでしょう。今回はその事を考えていきます。
結論として、AIとの会話をそのままブログにするのが一番なのではないかと思います。つまり、自分で書く場合と、AIとの会話をブログにする場合のハイブリッド型です。
ブログを書くのは面倒で、AIの出力、情報に劣ることもよくあります。実際、AIとの会話をそのままブログにしたいことが増えました。
とはいえ、情報の価値は思想よりも低いと思います。
多くの人がブログに求めるのは著者の思想ではないでしょうか。
`思想 > 情報`
したがって、AIを使うにしても、それが表現されていなければなりません。
## ailogの新機能
このことから、以下のような流れでブログを生成する仕組みを作りました。これは`ailog`の機能として実装し、`ailog`という単純なコマンドですべて処理されます。
```sh
$ ailog
```
1. 著者の思想をAIに質問する
2. 著者が作ったAIキャラクターが質問に答える
3. その会話をatprotoに投稿する
4. その会話をblogに表示する
とはいえ、会話は`claude`を使用します。依存関係が多すぎて汎用的な仕組みではありません。
これを汎用的な仕組みで作る場合、repositoryを分離して新しく作る必要があるでしょう。
example: [/posts/2025-07-16-6bf4b020.html](/posts/2025-07-16-6bf4b020.html)

View File

@@ -0,0 +1,64 @@
---
title: "ue5のgaspとdragonikを組み合わせてenemyを作る"
slug: "gasp-dragonik-enemy-chbcharacter"
date: "2025-07-30"
tags: ["ue"]
draft: false
---
ue5.6でgasp(game animation sample project)をベースにゲーム、特にキャラクターの操作を作っています。
そして、enemy(敵)を作り、バトルシーンを作成する予定ですが、これはどのように開発すればいいのでしょう。その方針を明確にします。
1. enemyもgaspの`cbp_character`に統合し、自キャラ、敵キャラどちらでも使用可能にする
2. 2番目のcharacterは動物型(type:animal)にし、gaspに統合する
3. enemyとして使用する場合は、enemy-AI-componentを追加するだけで完結する
4. characterのすべての操作を統一する
このようにすることで、応用可能なenemyを作ることができます。
例えば、`2番目のcharacterは動物型(type:animal)にする`というのはどういうことでしょう。
登場するキャラクターを人型(type:human), 動物型(type:animal)に分けるとして、動物型のテンプレートを作る必要があります。そのまま動物のmeshをgaspで使うと動きが変になってしまうので、それを調整する必要があるということ。そして、調整したものをテンプレート化して、他の動物にも適用できるようにしておくと、後の開発は楽ですよね。
ですから、早いうちにtype:humanから脱却し、他のtypeを作るほうがいいと判断しました。
これには、`dragon ik plugin`を使って、手っ取り早く動きを作ります。
`characterのすべての操作を統一する`というのは、1キャラにつき1属性、1通常攻撃、1スキル、1バースト、などのルールを作り、それらを共通化することです。共通化というのは、playerもenemy-AI-componentも違うキャラを同じ操作で使用できることを指します。
## 2番目のキャラクター
原作には、西洋ドラゴンのドライ(drai)というキャラが登場します。その父親が東洋ドラゴンのシンオウ(shin-oh)です。これをshinという名前で登録し、2番目のキャラクターとして設定しました。
3d-modelは今のところue5のcrsp(control rig sample project)にあるchinese dragonを使用しています。後に改造して原作に近づけるようにしたいところですが、今は時間が取れません。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/3c3Q1Z5r7QI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## データ構造の作成と適用
ゲームデータはatproto collection recordに保存して、そこからゲームに反映させたいと考えています。
まず基本データを`ai.syui.ai`のアカウントに保存。個別データをplayerのatprotoアカウントに保存する形が理想です。
基本データは、ゲームの基本的な設定のこと。例えば、キャラクターの名前や属性、スキルなど変更されることがない値。
個別データは、プレイヤーが使えるキャラ、レベル、攻撃力など、ゲームの進行とともに変更される値です。
ゲームをスタートさせると、まず基本データを取得し、それを`cbp_character`に適用します。ログインすると、`cbp_character`の変数(var)に値が振り分けられます。例えば、`skill-damage:0.0`があったとして、この値が変わります。
しかし、ゲームを開発していると、基本データも個別データも構造が複雑になります。
それを防ぐため、`{simple, core} mode`のような考え方を取り入れます。必要最小限の構成を分離、保存して、それをいつでも統合、適用できるように設計しておきます。
## gaspとdragonikを統合する方法
では、いよいよgaspとdragonikの統合手法を解説します。
まず、abpを作ります。それにdragonikを当て、それをSKM_Dragonのpost process animに指定します。
![](/img/ue_gasp_dragonik_shin_v0001.png)
次に、動きに合わせて首を上下させます。
<iframe src="https://blueprintue.com/render/piiw14oz" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>

View File

@@ -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!

View File

@@ -0,0 +1,7 @@
{{ $dateFormat := default "Mon Jan 2, 2006" (index .Site.Params "date_format") }}
{{ $utcFormat := "2006-01-02T15:04:05Z07:00" }}
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "description" .Description "categories" .Params.categories "contents" .Plain "href" .Permalink "utc_time" (.Date.Format $utcFormat) "formated_time" (.Date.Format $dateFormat)) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

View File

@@ -0,0 +1,14 @@
{{- $type := .Get "type" | default "info" -}}
{{- $content := .Get "content" -}}
<div class="msg msg-{{ $type }}">
<div class="msg-icon">
{{- if eq $type "info" -}}
{{- else if eq $type "warning" -}}⚠️
{{- else if eq $type "error" -}}❌
{{- else if eq $type "success" -}}✅
{{- else if eq $type "note" -}}📝
{{- else -}}
{{- end -}}
</div>
<div class="msg-content">{{ $content | markdownify }}</div>
</div>

View File

@@ -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: no-cache
/*.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

View File

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

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

View File

@@ -1,13 +1,13 @@
{
"client_id": "https://log.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_id": "https://syui.ai/client-metadata.json",
"client_name": "ai.log",
"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"
@@ -21,4 +21,4 @@
"subject_type": "public",
"application_type": "web",
"dpop_bound_access_tokens": true
}
}

345
my-blog/static/css/pds.css Normal file
View File

@@ -0,0 +1,345 @@
@import url('./style.css');
.pds-container {
}
.pds-header {
text-align: center;
margin-bottom: 40px;
}
.pds-header h1 {
font-size: 2.5em;
margin-bottom: 10px;
color: #333;
}
.pds-search-section {
border-radius: 8px;
}
.pds-search-form {
display: flex;
justify-content: center;
padding: 0px 20px;
}
.form-group {
display: flex;
align-items: center;
}
.form-group input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 14px;
width: 600px;
outline: none;
transition: box-shadow 0.2s, border-color 0.2s;
}
.form-group input:focus {
border-color: var(--theme-color, #f40);
}
.form-group button {
padding: 9px 15px;
background: #1976d2;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.form-group button:hover {
background: #1565c0;
}
/*
.user-info {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
*/
.user-profile {
display: flex;
align-items: center;
gap: 15px;
}
.user-details h3 {
margin: 0 0 5px 0;
color: #333;
}
.user-details p {
margin: 0;
color: #666;
}
.user-did-section {
margin: 20px 0;
}
.did-display {
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
color: #666;
word-break: break-all;
margin-bottom: 10px;
}
.handle-display {
padding: 8px 10px;
background: #f0f9f0;
border-radius: 4px;
font-size: 13px;
color: #555;
margin-bottom: 8px;
}
.handle-display strong {
color: #2e7d32;
}
.handle-display span {
font-family: monospace;
font-size: 12px;
color: #666;
word-break: break-all;
}
.pds-display {
padding: 8px 10px;
background: #e8f4f8;
border-radius: 4px;
font-size: 13px;
color: #555;
}
.pds-display strong {
color: #1976d2;
}
.pds-display span {
font-family: monospace;
font-size: 12px;
color: #666;
word-break: break-all;
}
.collections-section,
.records-section {
margin: 20px 0;
}
.collections-section h3,
.records-section h3 {
font-size: 1.2em;
margin-bottom: 15px;
color: #333;
font-weight: bold;
}
.collections-list,
.records-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.at-uri-link {
display: block;
padding: 8px 12px;
background: #f9f9f9;
border-radius: 4px;
border: 1px solid #e0e0e0;
color: #1976d2;
text-decoration: none;
font-family: monospace;
font-size: 14px;
word-break: break-all;
transition: all 0.2s;
}
.at-uri-link:hover {
background: #e8f4f8;
border-color: #1976d2;
text-decoration: none;
}
.pds-info {
padding: 8px 12px;
background: #f0f9ff;
border-radius: 4px;
border: 1px solid #b3e5fc;
margin-bottom: 8px;
color: #1976d2;
font-size: 12px;
}
.collection-info {
padding: 8px 12px;
background: #f0f9f0;
border-radius: 4px;
border: 1px solid #b3e5b3;
margin-bottom: 8px;
color: #2e7d32;
font-size: 12px;
}
.collections-header {
margin-bottom: 10px;
}
.collections-toggle {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.2s;
}
.collections-toggle:hover {
background: #e8f4f8;
border-color: #1976d2;
}
.pds-test-section,
.pds-about-section {
margin-bottom: 40px;
}
.pds-test-section h2,
.pds-about-section h2 {
font-size: 1.8em;
margin-bottom: 20px;
color: #333;
border-bottom: 2px solid #1976d2;
padding-bottom: 10px;
}
.test-uris {
display: flex;
flex-direction: column;
gap: 10px;
}
.at-uri {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 14px;
word-break: break-all;
cursor: pointer;
transition: background-color 0.2s;
border: 1px solid #e0e0e0;
}
.at-uri:hover {
background: #e8f4f8;
border-color: #1976d2;
}
.pds-about-section ul {
list-style-type: none;
padding: 0;
}
.pds-about-section li {
padding: 5px 0;
color: #666;
}
/* AT URI Modal Styles */
.at-uri-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.at-uri-modal-content {
background-color: white;
border-radius: 8px;
max-width: 800px;
max-height: 600px;
width: 90%;
height: 80%;
overflow: auto;
position: relative;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.at-uri-modal-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
z-index: 1001;
padding: 5px 10px;
}
/* Loading states */
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.error {
text-align: center;
padding: 20px;
color: #d32f2f;
background: #ffeaea;
border-radius: 4px;
margin: 10px 0;
}
/* Responsive design */
@media (max-width: 768px) {
.pds-search-section {
display: none;
}
.pds-search-form {
flex-direction: column;
align-items: stretch;
}
.form-group {
align-items: stretch;
}
.form-group input {
width: 100%;
margin-bottom: 10px;
}
}

File diff suppressed because it is too large Load Diff

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
my-blog/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

31
my-blog/static/index.json Normal file
View File

@@ -0,0 +1,31 @@
[
{
"categories": [],
"contents": "ブログを移行しました。過去のブログはsyui.github.ioにありあます。 gh-pagesからcf-pagesへの移行になります。 自作のailogでbuildしています。 特徴としては、atproto, AIとの連携です。 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: &#39;3&#39; url https://syui.pages.dev https://syui.github.io",
"description": "ブログを移行しました。過去のブログはsyui.github.ioにありあます。 \n\ngh-pagesからcf-pagesへの移行になります。\n自作のailogでbuildしています。\n特徴としては、atproto, AIとの連携です。\n\nname: Deploy to Cloudflare Pages\n\non:\n push:\n branches:\n - main\n workfl...",
"formated_time": "Sat Jun 14, 2025",
"href": "https://syui.ai/posts/2025-06-14-blog.html",
"tags": [
"blog",
"cloudflare",
"github"
],
"title": "ブログを移行した",
"utc_time": "2025-06-14T00:00:00Z"
},
{
"categories": [],
"contents": "rustで静的サイトジェネレータを作りました。ailogといいます。hugoからの移行になります。 ailogは、最初にatproto-comment-system(oauth)とask-AIという機能をつけました。 quick start $ git clone https://git.syui.ai/ai/log $ cd log $ cargo build $ ./target/debug/ailog init my-blog $ ./target/debug/ailog serve my-blog install $ cargo install --path . --- $ export CARGO_HOME=&quot;$HOME/.cargo&quot; $ export RUSTUP_HOME=&quot;$HOME/.rustup&quot; $ export PATH=&quot;$HOME/.cargo/bin:$PATH&quot; --- $ which ailog $ ailog -h build deploy $ cd my-blog $ vim config.toml $ ailog new test $ vim content/posts/`date +&quot;%Y-%m-%d&quot;`.md $ ailog build # publicの中身をweb-serverにdeploy $ cp -rf ./public/* ./web-server/root/ atproto-comment-system example $ cd ./oauth $ npm i $ npm run build $ npm run preview # Production environment variables VITE_APP_HOST=https://example.com VITE_OAUTH_CLIENT_ID=https://example.com/client-metadata.json VITE_OAUTH_REDIRECT_URI=https://example.com/oauth/callback VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn # Collection names for OAuth app VITE_COLLECTION_COMMENT=ai.syui.log VITE_COLLECTION_USER=ai.syui.log.user VITE_COLLECTION_CHAT=ai.syui.log.chat # Collection names for ailog (backward compatibility) AILOG_COLLECTION_COMMENT=ai.syui.log AILOG_COLLECTION_USER=ai.syui.log.user # API Configuration VITE_BSKY_PUBLIC_API=https://public.api.bsky.app これはailog oauth build my-blogで./my-blog/config.tomlから./oauth/.env.productionが生成されます。 $ ailog oauth build my-blog use 簡単に説明すると、./oauthで生成するのがatproto-comment-systemです。 &lt;script type=&quot;module&quot; crossorigin src=&quot;/assets/comment-atproto-${hash}}.js&quot;&gt;&lt;/script&gt; &lt;link rel=&quot;stylesheet&quot; crossorigin href=&quot;/assets/comment-atproto-${hash}.css&quot;&gt; &lt;section class=&quot;comment-section&quot;&gt; &lt;div id=&quot;comment-atproto&quot;&gt;&lt;/div&gt; &lt;/section&gt; ただし、oauthであるため、色々と大変です。本番環境(もしくは近い形)でテストを行いましょう。cf, tailscale, ngrokなど。 tunnel: ${hash} credentials-file: ${path}.json ingress: - hostname: example.com service: http://localhost:4173 originRequest: noHappyEyeballs: true - service: http_status:404 # tunnel list, dnsに登録が必要です $ cloudflared tunnel list $ cloudflared tunnel --config cloudflared-config.yml run $ cloudflared tunnel route dns ${uuid} example.com 以下の2つのcollection recordを生成します。ユーザーにはai.syui.logが生成され、ここにコメントが記録されます。それを取得して表示しています。ai.syui.log.userは管理者であるVITE_ADMIN_DID用です。 VITE_COLLECTION_COMMENT=ai.syui.log VITE_COLLECTION_USER=ai.syui.log.user $ ailog auth login $ ailog stream server このコマンドでai.syui.logをjetstreamから監視して、書き込みがあれば、管理者のai.syui.log.userに記録され、そのuser-listに基づいて、コメント一覧を取得します。 つまり、コメント表示のアカウントを手動で設定するか、自動化するか。自動化するならserverでailog stream serverを動かさなければいけません。 ask-AI ask-AIの仕組みは割愛します。後に変更される可能性が高いと思います。 local llm, mcp, atprotoと組み合わせです。 code syntax # comment d=${0:a:h} // This is a comment fn main() { println!(&quot;Hello, world!&quot;); } // This is a comment console.log(&quot;Hello, world!&quot;);",
"description": "rustで静的サイトジェネレータを作りました。ailogといいます。hugoからの移行になります。 \nailogは、最初にatproto-comment-system(oauth)とask-AIという機能をつけました。 \nquick start\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cargo build\n$ ./target/debu...",
"formated_time": "Thu Jun 12, 2025",
"href": "https://syui.ai/posts/2025-06-06-ailog.html",
"tags": [
"blog",
"rust",
"mcp",
"atp"
],
"title": "静的サイトジェネレータを作った",
"utc_time": "2025-06-12T00:00:00Z"
}
]

518
my-blog/static/js/ask-ai.js Normal file
View File

@@ -0,0 +1,518 @@
/**
* Ask AI functionality - Based on original working implementation
*/
// Global variables for AI functionality
let aiProfileData = null;
// Get config from window or use defaults
const OAUTH_PDS = window.OAUTH_CONFIG?.pds || 'syu.is';
const ADMIN_HANDLE = window.OAUTH_CONFIG?.admin || 'ai.syui.ai';
const OAUTH_COLLECTION = window.OAUTH_CONFIG?.collection || 'ai.syui.log';
// Listen for AI profile data from OAuth app
window.addEventListener('aiProfileLoaded', function(event) {
aiProfileData = event.detail;
updateAskAIButton();
});
// Check if AI profile data is already available
if (window.aiProfileData) {
aiProfileData = window.aiProfileData;
}
// Original functions from working implementation
function toggleAskAI() {
const panel = document.getElementById('askAiPanel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
// If AI profile data is already available, show introduction immediately
if (aiProfileData) {
// Quick check for authentication
const userSections = document.querySelectorAll('.user-section');
const isAuthenticated = userSections.length > 0;
handleAuthenticationStatus(isAuthenticated);
return;
}
// For production fallback - if OAuth app fails to load, show profiles
const isProd = window.location.hostname !== 'localhost' && !window.location.hostname.includes('preview');
if (isProd) {
// Shorter timeout for production
setTimeout(() => {
const userSections = document.querySelectorAll('.user-section');
if (userSections.length === 0) {
handleAuthenticationStatus(false);
} else {
handleAuthenticationStatus(true);
}
}, 300);
} else {
checkAuthenticationStatus();
}
}
}
function checkAuthenticationStatus() {
// Check multiple times for OAuth app to load
let checkCount = 0;
const maxChecks = 10;
const checkForAuth = () => {
const userSections = document.querySelectorAll('.user-section');
const authButtons = document.querySelectorAll('[data-auth-status]');
const oauthContainers = document.querySelectorAll('#oauth-container');
const isAuthenticated = userSections.length > 0;
if (isAuthenticated || checkCount >= maxChecks - 1) {
handleAuthenticationStatus(isAuthenticated);
} else {
checkCount++;
setTimeout(checkForAuth, 200);
}
};
checkForAuth();
}
function handleAuthenticationStatus(isAuthenticated) {
// Always hide loading first
document.getElementById('authCheck').style.display = 'none';
if (isAuthenticated) {
// User is authenticated - show Ask AI UI
document.getElementById('chatForm').style.display = 'block';
document.getElementById('chatHistory').style.display = 'block';
// Show initial greeting if chat history is empty and AI profile is available
const chatHistory = document.getElementById('chatHistory');
if (chatHistory.children.length === 0) {
if (aiProfileData) {
showInitialGreeting();
} else {
// Wait for AI profile data
setTimeout(() => {
if (aiProfileData) {
showInitialGreeting();
}
}, 500);
}
}
// Focus on input
setTimeout(() => {
document.getElementById('aiQuestion').focus();
}, 50);
} else {
// User not authenticated - show AI introduction directly if profile available
document.getElementById('chatForm').style.display = 'none';
document.getElementById('chatHistory').style.display = 'block';
if (aiProfileData) {
// Show AI introduction directly using available profile data
showAIIntroduction();
} else {
// Fallback to profile loading
loadAndShowProfiles();
}
}
}
// Load and display profiles from ai.syui.log.profile collection
async function loadAndShowProfiles() {
const chatHistory = document.getElementById('chatHistory');
chatHistory.innerHTML = '<div class="loading-message">Loading profiles...</div>';
try {
const response = await fetch(`https://${OAUTH_PDS}/xrpc/com.atproto.repo.listRecords?repo=${ADMIN_HANDLE}&collection=${OAUTH_COLLECTION}&limit=100`);
if (!response.ok) {
throw new Error('Failed to fetch profiles');
}
const data = await response.json();
// Filter only profile records and sort
const profileRecords = (data.records || []).filter(record => record.value.type === 'profile');
const profiles = profileRecords.sort((a, b) => {
if (a.value.profileType === 'admin' && b.value.profileType !== 'admin') return -1;
if (a.value.profileType !== 'admin' && b.value.profileType === 'admin') return 1;
return 0;
});
// Clear loading message
chatHistory.innerHTML = '';
// Display profiles using the same format as chat
profiles.forEach(profile => {
const profileDiv = document.createElement('div');
profileDiv.className = 'chat-message ai-message comment-style';
const avatarElement = profile.value.author.avatar
? `<img src="${profile.value.author.avatar}" alt="${profile.value.author.displayName || profile.value.author.handle}" class="profile-avatar">`
: `<div class="profile-avatar-fallback">${(profile.value.author.displayName || profile.value.author.handle || '?').charAt(0).toUpperCase()}</div>`;
const adminBadge = profile.value.profileType === 'admin'
? '<span class="admin-badge">Admin</span>'
: '';
profileDiv.innerHTML = `
<div class="message-header">
<div class="avatar">${avatarElement}</div>
<div class="user-info">
<div class="display-name">${profile.value.author.displayName || profile.value.author.handle} ${adminBadge}</div>
<div class="handle"><a href="https://${OAUTH_PDS}/profile/${profile.value.author.handle}" target="_blank" rel="noopener noreferrer">@${profile.value.author.handle}</a></div>
</div>
</div>
<div class="message-content">${profile.value.text}</div>
`;
chatHistory.appendChild(profileDiv);
});
if (profiles.length === 0) {
chatHistory.innerHTML = '<div class="no-profiles">No profiles available</div>';
}
} catch (error) {
chatHistory.innerHTML = '<div class="error-message">Failed to load profiles. Please try again later.</div>';
}
}
function askQuestion() {
const question = document.getElementById('aiQuestion').value;
if (!question.trim()) return;
const askButton = document.getElementById('askButton');
askButton.disabled = true;
askButton.textContent = 'Posting...';
try {
// Add user message to chat
addUserMessage(question);
// Clear input
document.getElementById('aiQuestion').value = '';
// Show loading
showLoadingMessage();
// Post question via OAuth app
window.dispatchEvent(new CustomEvent('postAIQuestion', {
detail: { question: question }
}));
} catch (error) {
showErrorMessage('Sorry, I encountered an error. Please try again.');
} finally {
askButton.disabled = false;
askButton.textContent = 'Ask';
}
}
function 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"><a href="https://${OAUTH_PDS}/profile/${userHandle}" target="_blank" rel="noopener noreferrer">@${userHandle}</a></div>
</div>
</div>
<div class="message-content">${question}</div>
`;
chatHistory.appendChild(questionDiv);
}
function showLoadingMessage() {
const chatHistory = document.getElementById('chatHistory');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'ai-loading-simple';
loadingDiv.innerHTML = `
<i class="fas fa-robot"></i>
<span>考えています</span>
<i class="fas fa-spinner fa-spin"></i>
`;
chatHistory.appendChild(loadingDiv);
}
function showErrorMessage(message) {
const chatHistory = document.getElementById('chatHistory');
removeLoadingMessage();
const errorDiv = document.createElement('div');
errorDiv.className = 'chat-message error-message comment-style';
errorDiv.innerHTML = `
<div class="message-header">
<div class="avatar">⚠️</div>
<div class="user-info">
<div class="display-name">System</div>
<div class="handle">@system</div>
<div class="timestamp">${new Date().toLocaleString()}</div>
</div>
</div>
<div class="message-content">${message}</div>
`;
chatHistory.appendChild(errorDiv);
}
function removeLoadingMessage() {
const loadingMsg = document.querySelector('.ai-loading-simple');
if (loadingMsg) {
loadingMsg.remove();
}
}
function showInitialGreeting() {
if (!aiProfileData) return;
const chatHistory = document.getElementById('chatHistory');
const greetingDiv = document.createElement('div');
greetingDiv.className = 'chat-message ai-message comment-style initial-greeting';
const avatarElement = aiProfileData.avatar
? `<img src="${aiProfileData.avatar}" alt="${aiProfileData.displayName}" class="profile-avatar">`
: '🤖';
greetingDiv.innerHTML = `
<div class="message-header">
<div class="avatar">${avatarElement}</div>
<div class="user-info">
<div class="display-name">${aiProfileData.displayName}</div>
<div class="handle"><a href="https://${OAUTH_PDS}/profile/${aiProfileData.handle}" target="_blank" rel="noopener noreferrer">@${aiProfileData.handle}</a></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);
}
function showAIIntroduction() {
if (!aiProfileData) return;
const chatHistory = document.getElementById('chatHistory');
chatHistory.innerHTML = ''; // Clear any existing content
// AI Introduction message
const introDiv = document.createElement('div');
introDiv.className = 'chat-message ai-message comment-style initial-greeting';
const avatarElement = aiProfileData.avatar
? `<img src="${aiProfileData.avatar}" alt="${aiProfileData.displayName}" class="profile-avatar">`
: '🤖';
introDiv.innerHTML = `
<div class="message-header">
<div class="avatar">${avatarElement}</div>
<div class="user-info">
<div class="display-name">${aiProfileData.displayName}</div>
<div class="handle"><a href="https://${OAUTH_PDS}/profile/${aiProfileData.handle}" target="_blank" rel="noopener noreferrer">@${aiProfileData.handle}</a></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(introDiv);
// OAuth login message
const loginDiv = document.createElement('div');
loginDiv.className = 'chat-message user-message comment-style initial-greeting';
loginDiv.innerHTML = `
<div class="message-header">
<div class="avatar">${avatarElement}</div>
<div class="user-info">
<div class="display-name">${aiProfileData.displayName}</div>
<div class="handle"><a href="https://${OAUTH_PDS}/profile/${aiProfileData.handle}" target="_blank" rel="noopener noreferrer">@${aiProfileData.handle}</a></div>
</div>
</div>
<div class="message-content">Please atproto oauth login</div>
`;
chatHistory.appendChild(loginDiv);
}
function updateAskAIButton() {
const button = document.getElementById('askAiButton');
if (!button) return;
// Only update text, never modify the icon
if (aiProfileData && aiProfileData.displayName) {
const textNode = button.childNodes[2] || button.lastChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
textNode.textContent = aiProfileData.displayName;
}
}
}
function handleAIResponse(responseData) {
const chatHistory = document.getElementById('chatHistory');
removeLoadingMessage();
const aiProfile = responseData.aiProfile;
if (!aiProfile || !aiProfile.handle || !aiProfile.displayName) {
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"><a href="https://${OAUTH_PDS}/profile/${aiProfile.handle}" target="_blank" rel="noopener noreferrer">@${aiProfile.handle}</a></div>
</div>
</div>
<div class="message-content">${responseData.answer}</div>
`;
chatHistory.appendChild(answerDiv);
// Limit chat history
limitChatHistory();
}
function limitChatHistory() {
const chatHistory = document.getElementById('chatHistory');
if (chatHistory.children.length > 10) {
chatHistory.removeChild(chatHistory.children[0]);
if (chatHistory.children.length > 0) {
chatHistory.removeChild(chatHistory.children[0]);
}
}
}
// Event listeners setup
function setupAskAIEventListeners() {
// Listen for AI profile updates from OAuth app
window.addEventListener('aiProfileLoaded', function(event) {
aiProfileData = event.detail;
updateAskAIButton();
});
// Listen for AI responses
window.addEventListener('aiResponseReceived', function(event) {
handleAIResponse(event.detail);
});
// Listen for OAuth callback completion from iframe
window.addEventListener('message', function(event) {
if (event.data.type === 'oauth_success') {
// Close any OAuth popups/iframes
const oauthFrame = document.getElementById('oauth-frame');
if (oauthFrame) {
oauthFrame.remove();
}
// Reload the page to refresh OAuth app state
setTimeout(() => {
window.location.reload();
}, 500);
}
});
// Track IME composition state
let isComposing = false;
const aiQuestionInput = document.getElementById('aiQuestion');
if (aiQuestionInput) {
aiQuestionInput.addEventListener('compositionstart', function() {
isComposing = true;
});
aiQuestionInput.addEventListener('compositionend', function() {
isComposing = false;
});
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const panel = document.getElementById('askAiPanel');
if (panel) {
panel.style.display = 'none';
}
}
// Enter key to send message (only when not composing Japanese input)
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey && !isComposing) {
e.preventDefault();
askQuestion();
}
});
}
// Initialize Ask AI when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
setupAskAIEventListeners();
// Also listen for OAuth app load completion
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
// Check if user-section was added/removed
const userSectionAdded = Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList?.contains('user-section') || node.querySelector?.('.user-section'))
);
const userSectionRemoved = Array.from(mutation.removedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList?.contains('user-section') || node.querySelector?.('.user-section'))
);
if (userSectionAdded || userSectionRemoved) {
// Update Ask AI panel if it's visible
const panel = document.getElementById('askAiPanel');
if (panel && panel.style.display !== 'none') {
checkAuthenticationStatus();
}
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
// Global functions for onclick handlers
window.toggleAskAI = toggleAskAI;
window.askQuestion = askQuestion;

View File

@@ -0,0 +1,123 @@
/**
* Image Comparison Slider
* UE5-style before/after image comparison component
*/
class ImageComparison {
constructor(container) {
this.container = container;
this.slider = container.querySelector('.slider');
this.beforeImg = container.querySelector('.img-before');
this.afterImg = container.querySelector('.img-after');
this.sliderThumb = container.querySelector('.slider-thumb');
this.isDragging = false;
this.containerRect = null;
this.init();
}
init() {
this.bindEvents();
this.updatePosition(50); // Start at 50%
}
bindEvents() {
// Mouse events
this.slider.addEventListener('input', (e) => {
this.updatePosition(e.target.value);
});
this.slider.addEventListener('mousedown', () => {
this.isDragging = true;
document.body.style.userSelect = 'none';
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
document.body.style.userSelect = '';
}
});
// Touch events for mobile
this.slider.addEventListener('touchstart', (e) => {
this.isDragging = true;
e.preventDefault();
});
this.slider.addEventListener('touchmove', (e) => {
if (this.isDragging) {
const touch = e.touches[0];
this.containerRect = this.container.getBoundingClientRect();
const x = touch.clientX - this.containerRect.left;
const percentage = Math.max(0, Math.min(100, (x / this.containerRect.width) * 100));
this.slider.value = percentage;
this.updatePosition(percentage);
e.preventDefault();
}
});
this.slider.addEventListener('touchend', () => {
this.isDragging = false;
});
// Direct click on container
this.container.addEventListener('click', (e) => {
if (e.target === this.container || e.target.classList.contains('img-comparison-slider')) {
this.containerRect = this.container.getBoundingClientRect();
const x = e.clientX - this.containerRect.left;
const percentage = Math.max(0, Math.min(100, (x / this.containerRect.width) * 100));
this.slider.value = percentage;
this.updatePosition(percentage);
}
});
// Keyboard support
this.slider.addEventListener('keydown', (e) => {
let value = parseFloat(this.slider.value);
switch (e.key) {
case 'ArrowLeft':
value = Math.max(0, value - 1);
break;
case 'ArrowRight':
value = Math.min(100, value + 1);
break;
case 'Home':
value = 0;
break;
case 'End':
value = 100;
break;
default:
return;
}
e.preventDefault();
this.slider.value = value;
this.updatePosition(value);
});
}
updatePosition(percentage) {
const position = parseFloat(percentage);
// Update clip-path for before image to show only the left portion
this.beforeImg.style.clipPath = `inset(0 ${100 - position}% 0 0)`;
// Update slider thumb position
this.sliderThumb.style.left = `${position}%`;
this.sliderThumb.style.transform = `translateX(-50%)`;
}
}
// Auto-initialize all image comparison components
document.addEventListener('DOMContentLoaded', function() {
const comparisons = document.querySelectorAll('.img-comparison-container');
comparisons.forEach(container => {
new ImageComparison(container);
});
});
// Export for manual initialization
window.ImageComparison = ImageComparison;

370
my-blog/static/js/pds.js Normal file
View File

@@ -0,0 +1,370 @@
// AT Protocol API functions
const AT_PROTOCOL_CONFIG = {
primary: {
pds: 'https://syu.is',
plc: 'https://plc.syu.is',
bsky: 'https://bsky.syu.is',
web: 'https://web.syu.is'
},
fallback: {
pds: 'https://bsky.social',
plc: 'https://plc.directory',
bsky: 'https://public.api.bsky.app',
web: 'https://bsky.app'
}
};
// Search user function
async function searchUser() {
const handleInput = document.getElementById('handleInput');
const userInfo = document.getElementById('userInfo');
const collectionsList = document.getElementById('collectionsList');
const recordsList = document.getElementById('recordsList');
const searchButton = document.getElementById('searchButton');
const input = handleInput.value.trim();
if (!input) {
alert('Handle nameまたはAT URIを入力してください');
return;
}
searchButton.disabled = true;
searchButton.innerHTML = '@';
//searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
try {
// Clear previous results
document.getElementById('userDidSection').style.display = 'none';
document.getElementById('collectionsSection').style.display = 'none';
document.getElementById('recordsSection').style.display = 'none';
collectionsList.innerHTML = '';
recordsList.innerHTML = '';
// Check if input is AT URI
if (input.startsWith('at://')) {
// Parse AT URI to check if it's a full record or just a handle/collection
const uriParts = input.replace('at://', '').split('/').filter(part => part.length > 0);
if (uriParts.length >= 3) {
// Full AT URI with rkey - show in modal
showAtUriModal(input);
return;
} else if (uriParts.length === 1) {
// Just handle in AT URI format (at://handle) - treat as regular handle
const handle = uriParts[0];
const userProfile = await resolveUserProfile(handle);
if (userProfile.success) {
displayUserDid(userProfile.data);
await loadUserCollections(handle, userProfile.data.did);
} else {
alert('ユーザーが見つかりません: ' + userProfile.error);
}
return;
} else if (uriParts.length === 2) {
// Collection level AT URI - load collection records
const [repo, collection] = uriParts;
try {
// First resolve the repo to get handle if it's a DID
let handle = repo;
if (repo.startsWith('did:')) {
// Try to resolve DID to handle - for now just use the DID
handle = repo;
}
loadCollectionRecords(handle, collection, repo);
} catch (error) {
alert('コレクションの読み込みに失敗しました: ' + error.message);
}
return;
}
}
// Handle regular handle search
const userProfile = await resolveUserProfile(input);
if (userProfile.success) {
displayUserDid(userProfile.data);
await loadUserCollections(input, userProfile.data.did);
} else {
alert('ユーザーが見つかりません: ' + userProfile.error);
}
} catch (error) {
alert('エラーが発生しました: ' + error.message);
} finally {
searchButton.disabled = false;
searchButton.innerHTML = '@';
//searchButton.innerHTML = '<i class="fab fa-bluesky"></i>';
}
}
// Resolve user profile
async function resolveUserProfile(handle) {
try {
let response = null;
// Try syu.is first
try {
response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
} catch (error) {
console.log('Failed to resolve from syu.is:', error);
}
// If syu.is fails, try bsky.social
if (!response || !response.ok) {
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
}
if (!response.ok) {
throw new Error('Failed to resolve handle');
}
const repoData = await response.json();
// Get profile data
const profileResponse = await fetch(`${AT_PROTOCOL_CONFIG.fallback.bsky}/xrpc/app.bsky.actor.getProfile?actor=${repoData.did}`);
const profileData = await profileResponse.json();
return {
success: true,
data: {
did: repoData.did,
handle: profileData.handle,
displayName: profileData.displayName,
avatar: profileData.avatar,
description: profileData.description,
pds: repoData.didDoc.service.find(s => s.type === 'AtprotoPersonalDataServer')?.serviceEndpoint
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// Display user DID
function displayUserDid(profile) {
document.getElementById('userPdsText').textContent = profile.pds || 'Unknown';
document.getElementById('userHandleText').textContent = profile.handle;
document.getElementById('userDidText').textContent = profile.did;
document.getElementById('userDidSection').style.display = 'block';
}
// Load user collections
async function loadUserCollections(handle, did) {
const collectionsList = document.getElementById('collectionsList');
collectionsList.innerHTML = '<div class="loading">コレクションを読み込み中...</div>';
try {
// Try to get collections from describeRepo
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
// If syu.is fails, try bsky.social
if (!response.ok) {
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.describeRepo?repo=${handle}`);
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
}
if (!response.ok) {
throw new Error('Failed to describe repository');
}
const data = await response.json();
const collections = data.collections || [];
// Display collections as AT URI links
collectionsList.innerHTML = '';
if (collections.length === 0) {
collectionsList.innerHTML = '<div class="error">コレクションが見つかりませんでした</div>';
} else {
collections.forEach(collection => {
const atUri = `at://${did}/${collection}/`;
const collectionElement = document.createElement('a');
collectionElement.className = 'at-uri-link';
collectionElement.href = '#';
collectionElement.textContent = atUri;
collectionElement.onclick = (e) => {
e.preventDefault();
loadCollectionRecords(handle, collection, did);
// Close collections and update toggle
document.getElementById('collectionsList').style.display = 'none';
document.getElementById('collectionsToggle').textContent = '[-] Collections';
};
collectionsList.appendChild(collectionElement);
});
document.getElementById('collectionsSection').style.display = 'block';
}
} catch (error) {
collectionsList.innerHTML = '<div class="error">コレクションの読み込みに失敗しました: ' + error.message + '</div>';
document.getElementById('collectionsSection').style.display = 'block';
}
}
// Load collection records
async function loadCollectionRecords(handle, collection, did) {
const recordsList = document.getElementById('recordsList');
recordsList.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
try {
// Try with syu.is first
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
let usedPds = AT_PROTOCOL_CONFIG.primary.pds;
// If that fails, try with bsky.social
if (!response.ok) {
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=${collection}`);
usedPds = AT_PROTOCOL_CONFIG.fallback.pds;
}
if (!response.ok) {
throw new Error('Failed to load records');
}
const data = await response.json();
// Display records as AT URI links
recordsList.innerHTML = '';
// Add collection info for records
const collectionInfo = document.createElement('div');
collectionInfo.className = 'collection-info';
collectionInfo.innerHTML = `<strong>${collection}</strong>`;
recordsList.appendChild(collectionInfo);
data.records.forEach(record => {
const atUri = record.uri;
const recordElement = document.createElement('a');
recordElement.className = 'at-uri-link';
recordElement.href = '#';
recordElement.textContent = atUri;
recordElement.onclick = (e) => {
e.preventDefault();
showAtUriModal(atUri);
};
recordsList.appendChild(recordElement);
});
document.getElementById('recordsSection').style.display = 'block';
} catch (error) {
recordsList.innerHTML = '<div class="error">レコードの読み込みに失敗しました: ' + error.message + '</div>';
document.getElementById('recordsSection').style.display = 'block';
}
}
// Show AT URI modal
function showAtUriModal(uri) {
const modal = document.getElementById('atUriModal');
const content = document.getElementById('atUriContent');
content.innerHTML = '<div class="loading">レコードを読み込み中...</div>';
modal.style.display = 'flex';
// Load record data
loadAtUriRecord(uri, content);
}
// Load AT URI record
async function loadAtUriRecord(uri, contentElement) {
try {
const parts = uri.replace('at://', '').split('/');
const repo = parts[0];
const collection = parts[1];
const rkey = parts[2];
// Try with syu.is first
let response = await fetch(`${AT_PROTOCOL_CONFIG.primary.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
// If that fails, try with bsky.social
if (!response.ok) {
response = await fetch(`${AT_PROTOCOL_CONFIG.fallback.pds}/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${collection}&rkey=${rkey}`);
}
if (!response.ok) {
throw new Error('Failed to load record');
}
const data = await response.json();
contentElement.innerHTML = `
<div style="padding: 20px;">
<h3>AT URI Record</h3>
<div style="font-family: monospace; font-size: 14px; color: #666; margin-bottom: 20px; word-break: break-all;">
${uri}
</div>
<div style="font-size: 12px; color: #999; margin-bottom: 20px;">
Repo: ${repo} | Collection: ${collection} | RKey: ${rkey}
</div>
<h4>Record Data</h4>
<pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
</div>
`;
} catch (error) {
contentElement.innerHTML = `
<div style="padding: 20px; color: red;">
<strong>Error:</strong> ${error.message}
<div style="margin-top: 10px; font-size: 12px;">
<strong>URI:</strong> ${uri}
</div>
</div>
`;
}
}
// Close AT URI modal
function closeAtUriModal(event) {
const modal = document.getElementById('atUriModal');
if (event && event.target !== modal) {
return;
}
modal.style.display = 'none';
}
// Initialize AT URI click handlers
document.addEventListener('DOMContentLoaded', function() {
// Add click handlers to existing AT URIs
document.querySelectorAll('.at-uri').forEach(element => {
element.addEventListener('click', function() {
const uri = this.getAttribute('data-at-uri');
showAtUriModal(uri);
});
});
// ESC key to close modal
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeAtUriModal();
}
});
// Enter key to search
document.getElementById('handleInput').addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
searchUser();
}
});
});
// Toggle collections visibility
function toggleCollections() {
const collectionsList = document.getElementById('collectionsList');
const toggleButton = document.getElementById('collectionsToggle');
if (collectionsList.style.display === 'none') {
collectionsList.style.display = 'block';
toggleButton.textContent = '[-] Collections';
} else {
collectionsList.style.display = 'none';
toggleButton.textContent = '[+] Collections';
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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
}
}
// Initialize theme when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new Theme();
});

View File

@@ -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-B2YEFA6R.js"></script>
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-BHjafP79.css">

View 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.**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

View 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
View 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

View File

@@ -0,0 +1,61 @@
<!-- AT Browser Integration - Temporarily disabled to fix site display -->
<!--
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="/assets/pds-browser.umd.js"></script>
<script>
// AT Browser integration - needs debugging
console.log('AT Browser integration temporarily disabled');
</script>
-->
<style>
/* AT Browser Modal Styles */
.at-uri-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.at-uri-modal-content {
background-color: white;
border-radius: 8px;
max-width: 800px;
max-height: 600px;
width: 90%;
height: 80%;
overflow: auto;
position: relative;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.at-uri-modal-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
z-index: 1001;
padding: 5px 10px;
}
/* AT URI Link Styles */
[data-at-uri] {
color: #1976d2;
cursor: pointer;
text-decoration: underline;
}
[data-at-uri]:hover {
color: #1565c0;
}
</style>

View File

@@ -4,10 +4,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
{% include "oauth-assets.html" %}
<!-- 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="/css/svg-animation-package.css">
<link rel="stylesheet" href="/css/pds.css">
<link rel="stylesheet" href="/pkg/icomoon/style.css">
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css">
{% block head %}{% endblock %}
</head>
<body>
@@ -15,36 +23,75 @@
<header class="main-header">
<div class="header-content">
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
<div class="header-actions">
<!-- Ask AI button on all pages -->
<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">
<!-- User Handle Input Form -->
<div class="pds-search-section">
<form class="pds-search-form" onsubmit="searchUser(); return false;">
<div class="form-group">
<input type="text" id="handleInput" placeholder="at://syui.ai" value="syui.ai" />
<button type="submit" id="searchButton" class="pds-btn">
@
</button>
</div>
</form>
</div>
<button class="ask-ai-btn" onclick="toggleAskAI()" id="askAiButton">
<span class="ai-icon">🤖</span>
Ask AI
<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 class="loading-content">
<div class="loading-spinner"></div>
<p>Loading...</p>
</div>
</div>
<!-- Chat form (hidden until authenticated) -->
<div id="chatForm" class="ask-ai-form" style="display: none;">
<input type="text" id="aiQuestion" placeholder="What would you like to know?" />
<button onclick="askQuestion()" id="askButton">Ask</button>
</div>
<!-- Chat history -->
<div id="chatHistory" class="chat-history" style="display: none;"></div>
</div>
</div>
<main class="main-content">
<!-- Pds Panel -->
{% include "pds-header.html" %}
{% block content %}{% endblock %}
</main>
@@ -52,309 +99,54 @@
</div>
<footer class="main-footer">
<p>&copy; {{ config.author }}</p>
<div class="footer-social">
<a href="https://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://github.com/syui" target="_blank"><i class="fab fa-github"></i></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]);
}
}
}
// Config variables from Hugo
window.OAUTH_CONFIG = {
{% if config.oauth.pds %}
pds: "{{ config.oauth.pds }}",
{% else %}
pds: "syu.is",
{% endif %}
{% if config.oauth.admin %}
admin: "{{ config.oauth.admin }}",
{% else %}
admin: "ai.syui.ai",
{% endif %}
{% if config.oauth.collection %}
collection: "{{ config.oauth.collection }}"
{% else %}
collection: "ai.syui.log"
{% endif %}
};
</script>
<script src="/js/ask-ai.js"></script>
<script src="/js/pds.js"></script>
<script src="/js/theme.js"></script>
<script src="/js/image-comparison.js"></script>
<!-- Mermaid support -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'neutral',
securityLevel: 'loose',
themeVariables: {
fontFamily: 'system-ui, -apple-system, sans-serif',
fontSize: '14px'
}
});
</script>
{% include "oauth-assets.html" %}
{% include "at-browser-assets.html" %}
</body>
</html>
</html>

135
my-blog/templates/game.html Normal file
View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}Game - {{ config.title }}{% endblock %}
{% block content %}
<div id="gameContainer" class="game-container">
<div id="gameAuth" class="game-auth-section">
<h1>Login to Play</h1>
<p>Please authenticate with your AT Protocol account to access the game.</p>
<div id="authRoot"></div>
</div>
<div id="gameFrame" class="game-frame-container" style="display: none;">
<iframe
id="pixelStreamingFrame"
src="https://verse.syui.ai/simple-noui.html"
frameborder="0"
allowfullscreen
allow="microphone; camera; fullscreen; autoplay"
class="pixel-streaming-iframe"
></iframe>
</div>
</div>
<style>
/* Game specific styles */
.game-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: #000;
overflow: hidden;
}
.game-auth-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: white;
}
.game-auth-section h1 {
font-size: 2.5em;
margin-bottom: 20px;
color: #fff;
}
.game-auth-section p {
font-size: 1.2em;
margin-bottom: 30px;
color: #ccc;
}
.game-frame-container {
width: 100%;
height: 100vh;
position: relative;
}
.pixel-streaming-iframe {
width: 100%;
height: 100%;
border: none;
}
/* Override auth button for game page */
.game-auth-section .auth-section {
background: transparent;
box-shadow: none;
}
.game-auth-section .auth-button {
font-size: 1.2em;
padding: 12px 30px;
}
/* Hide header and footer on game page */
body:has(.game-container) header,
body:has(.game-container) footer,
body:has(.game-container) nav {
display: none !important;
}
/* Remove any body padding/margin for full screen game */
body:has(.game-container) {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<script>
// Wait for OAuth component to be loaded
document.addEventListener('DOMContentLoaded', function() {
// Check if user is already authenticated
const checkAuthStatus = () => {
// Check if OAuth components are available and user is authenticated
if (window.currentUser && window.currentAgent) {
showGame();
return true;
}
return false;
};
// Show game iframe
const showGame = () => {
document.getElementById('gameAuth').style.display = 'none';
document.getElementById('gameFrame').style.display = 'block';
};
// Listen for OAuth success
window.addEventListener('oauth-success', function(event) {
console.log('OAuth success:', event.detail);
showGame();
});
// Check auth status on load
if (!checkAuthStatus()) {
// Check periodically if OAuth components are loaded
const authCheckInterval = setInterval(() => {
if (checkAuthStatus()) {
clearInterval(authCheckInterval);
}
}, 500);
}
});
</script>
<!-- Include OAuth assets -->
{% include "oauth-assets.html" %}
{% endblock %}

View File

@@ -20,19 +20,6 @@
<a href="{{ post.url }}">{{ post.title }}</a>
</h3>
{% if post.excerpt %}
<p class="post-excerpt">{{ post.excerpt }}</p>
{% endif %}
<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>
{% endif %}
{% if post.translation_url %}
<a href="{{ post.translation_url }}" class="view-translation" title="View Translation">🌐</a>
{% endif %}
</div>
</div>
</article>
{% endfor %}

View File

@@ -1,3 +0,0 @@
<!-- 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">

View File

@@ -0,0 +1,48 @@
<div class="pds-container">
<div class="pds-header">
</div>
<!-- Current User DID -->
<div id="userDidSection" class="user-did-section" style="display: none;">
<div class="pds-display">
<strong>PDS:</strong> <span id="userPdsText"></span>
</div>
<div class="handle-display">
<strong>Handle:</strong> <span id="userHandleText"></span>
</div>
<div class="did-display">
<span id="userDidText"></span>
</div>
</div>
<!-- Collection List -->
<div id="collectionsSection" class="collections-section" style="display: none;">
<div class="collections-header">
<button id="collectionsToggle" class="collections-toggle" onclick="toggleCollections()">[+] Collections</button>
</div>
<div id="collectionsList" class="collections-list" style="display: none;">
<!-- Collections will be populated here -->
</div>
</div>
<!-- AT URI Records -->
<div id="recordsSection" class="records-section" style="display: none;">
<div id="recordsList" class="records-list">
<!-- Records will be populated here -->
</div>
</div>
</div>
<!-- AT URI Modal -->
<div id="atUriModal" class="at-uri-modal-overlay" style="display: none;" onclick="closeAtUriModal(event)">
<div class="at-uri-modal-content">
<button class="at-uri-modal-close" onclick="closeAtUriModal()">&times;</button>
<div id="atUriContent"></div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block title %}at-uri browser - {{ config.title }}{% endblock %}
{% block content %}
{% endblock %}

View File

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

View File

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

View File

@@ -13,10 +13,11 @@
<span class="article-lang">{{ post.language }}</span>
{% endif %}
</div>
{% if not post.extra.type or post.extra.type != "ai" %}
<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 %}
@@ -25,29 +26,35 @@
</a>
{% endif %}
</div>
{% endif %}
</header>
<div class="article-body">
{{ post.content | safe }}
</div>
<div id="comment-atproto"></div>
</article>
<aside class="article-sidebar">
{% if not post.extra.type or post.extra.type != "ai" %}
<nav class="toc">
<h3>Contents</h3>
<div id="toc-content">
<!-- TOC will be generated by JavaScript -->
</div>
</nav>
</aside>
<div class="article-body">
{{ post.content | safe }}
</div>
{% endif %}
<div id="comment-atproto"></div>
</article>
</div>
<script>
// Generate table of contents
function generateTableOfContents() {
const tocContainer = document.getElementById('toc-content');
if (!tocContainer) {
return;
}
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
if (headings.length === 0) {

10
oauth/.env Normal file
View File

@@ -0,0 +1,10 @@
VITE_ADMIN=ai.syui.ai
VITE_PDS=syu.is
VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"]
VITE_COLLECTION=ai.syui.log
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
# Development/Debug features
VITE_ENABLE_TEST_UI=true
VITE_ENABLE_DEBUG=true

View File

@@ -1,28 +1,19 @@
# 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_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
VITE_ADMIN=ai.syui.ai
VITE_PDS=syu.is
VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"]
VITE_COLLECTION=ai.syui.log
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
# Collection names for OAuth app
VITE_COLLECTION_COMMENT=ai.syui.log
VITE_COLLECTION_USER=ai.syui.log.user
VITE_COLLECTION_CHAT=ai.syui.log.chat
# Collection names for ailog (backward compatibility)
AILOG_COLLECTION_COMMENT=ai.syui.log
AILOG_COLLECTION_USER=ai.syui.log.user
AILOG_COLLECTION_CHAT=ai.syui.log.chat
# AI Configuration
# AI Configuration - match oauth_old settings
VITE_AI_ENABLED=true
VITE_AI_ASK_AI=true
VITE_AI_PROVIDER=ollama
VITE_AI_MODEL=gemma3:4b
VITE_AI_MODEL=gemma3:1b
VITE_AI_HOST=https://ollama.syui.ai
VITE_ASK_AI_URL=https://ollama.syui.ai/api/generate
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
# API Configuration
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
# Production settings - Disable development features
VITE_ENABLE_TEST_UI=false
VITE_ENABLE_DEBUG=true

25
oauth/build-minimal.js Normal file
View File

@@ -0,0 +1,25 @@
// Create minimal index.html like oauth/dist/index.html format
import fs from 'fs'
import path from 'path'
const distDir = './dist'
const indexPath = path.join(distDir, 'index.html')
// Read the built index.html
const content = fs.readFileSync(indexPath, 'utf8')
// Extract script and link tags
const scriptMatch = content.match(/<script[^>]*src="([^"]*)"[^>]*><\/script>/)
const linkMatch = content.match(/<link[^>]*href="([^"]*)"[^>]*>/)
if (scriptMatch && linkMatch) {
const minimalContent = `<!-- OAuth Comment System - Load globally for session management -->
<script type="module" crossorigin src="${scriptMatch[1]}"></script>
<link rel="stylesheet" crossorigin href="${linkMatch[1]}">
`
fs.writeFileSync(indexPath, minimalContent)
console.log('Generated minimal index.html')
} else {
console.error('Could not extract asset references')
}

View File

@@ -1,20 +1,11 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ai.card</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0a0a0a;
color: #ffffff;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<html>
<head>
<meta charset="UTF-8">
<title>Comments Test</title>
</head>
<body>
<div id="comment-atproto"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,62 @@
{
"records": [
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.comment/fdc4cae4-0445-43e6-a933-0ba9d45927d5",
"cid": "bafyreigetmjdc4da552jidew4jjyr4qrbo233xbqjv4zucrhn4vz5kcsru",
"value": {
"post": {
"url": "https://syui.ai/posts/2025-06-06-ailog.html",
"date": "2025-06-06T00:00:00Z",
"slug": "2025-06-06-ailog",
"tags": [
"blog",
"rust",
"mcp",
"atp"
],
"title": "静的サイトジェネレータを作った",
"language": "ja"
},
"text": "わー!すごい!✨ 宇宙みたいにプログラムが組み合わさって、ブログが作れるんだ!まるで、小さな星たちがダンスを踊るみたいでしょ?アイルー!🚀",
"type": "info",
"$type": "ai.syui.log.chat.comment",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T08:56:15.630183+00:00"
}
},
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.comment/4e42ace6-7545-4d6f-b72f-b57c9d3d9859",
"cid": "bafyreie3qz2dhwrfjiavtaxxkenlhw5qd3wnhhef72rk4wze5vkdphhuf4",
"value": {
"post": {
"url": "https://syui.ai/posts/2025-06-14-blog.html",
"date": "2025-06-14T00:00:00Z",
"slug": "2025-06-14-blog",
"tags": [
"blog",
"cloudflare",
"github"
],
"title": "ブログを移行した",
"language": "ja"
},
"text": "わーブログ、変わったねAIと繋がるとか、すごーく、すごく、すっごいまるで魔法みたい✨ 小さなものにも、ちゃんと名前があるんだ!うれしい!💖",
"type": "info",
"$type": "ai.syui.log.chat.comment",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T08:55:55.836221+00:00"
}
}
],
"cursor": "4e42ace6-7545-4d6f-b72f-b57c9d3d9859"
}

View File

@@ -0,0 +1,62 @@
{
"records": [
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.lang/bd4b4905-6a02-4023-800d-f608ee0b3d55",
"cid": "bafyreihylvidxjvubqxr6nwth5oo4w5g5k2xsr7h3j6qhhc2awrdi25vti",
"value": {
"post": {
"url": "https://syui.ai/posts/2025-06-06-ailog.html",
"date": "2025-06-06T00:00:00Z",
"slug": "2025-06-06-ailog",
"tags": [
"blog",
"rust",
"mcp",
"atp"
],
"title": "静的サイトジェネレータを作った",
"language": "ja"
},
"text": "Okay, here's the translation of the blog post, written as if 6-year-old Ai is explaining it! ✨\n\n---\n\n**My Awesome Blog Thing Called \"ailog\"!**\n\nHi everyone! Im Ai, and I made a super cool blog! Its called “ailog,” and its like a secret clubhouse for my thoughts! 💖\n\n**How I Built It (It's a Little Bit Complicated!)**\n\nFirst, I used something called “Rust.” Its like a magic toolbox for making computer programs! 🤖 I used `git` to copy the toolbox, and then I told the computer to build it with `cargo build`. Its like building with LEGOs!\n\n**Adding Cool Features!**\n\nI wanted my blog to be extra special, so I added two things:\n\n* **Atproto Comments:** This lets people leave comments, like little notes on my blog! Its like having a chat with my friends! I used something called `npm` to help with this.\n* **Ask-AI:** This is a super smart robot that can answer my questions! Its like having a really, really good friend who knows everything! (But it's still changing, so maybe it wont always be perfect!)\n\n**How to Make My Blog Work!**\n\n1. I made a special file called `config.toml`. It's like the secret recipe for my blog!\n2. I used a thing called `ailog` to make all the pieces fit together. It's like putting the LEGOs in the right spots!\n3. I needed to tell the computer where to put my blog so people could see it! I used a thing called `cloudflared` to make it easy.\n\n**Important Stuff (Dont Worry, Ill Explain!)**\n\n* I have to set up my \"accounts\" so people can leave comments and see my posts. It's a little tricky, but I'm working on it!\n* I'm watching a special file called `/index.json` to see if anything changes. If it does, I can automatically translate things and make new posts! Isnt that amazing?!\n\n**Testing, Testing, 1, 2, 3!**\n\nI need to test my blog to make sure everything works perfectly. Ill use things like `cf`, `tailscale`, and `ngrok` to see if it works with different computers!\n\n---\n\n**Notes from Ai!** 📝\n\n* I love making things! It's so much fun!\n* I hope you like my blog! 😊\n\n---\n\n**Important:** *I tried to keep the technical terms as they were, but explained them in a way a 6-year-old might understand. I also added some of Ais personality throughout!*",
"type": "en",
"$type": "ai.syui.log.chat.lang",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T08:56:14.115881+00:00"
}
},
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.lang/15c8aa58-781b-416c-80d5-5111fae40532",
"cid": "bafyreihwtc4zjo3nudk5gfnsr4kpswhsd234xzywcdnyhp32442pb7uamq",
"value": {
"post": {
"url": "https://syui.ai/posts/2025-06-14-blog.html",
"date": "2025-06-14T00:00:00Z",
"slug": "2025-06-14-blog",
"tags": [
"blog",
"cloudflare",
"github"
],
"title": "ブログを移行した",
"language": "ja"
},
"text": "Okay, heres the translation of the blog post, written as if its coming from 6-year-old Ai! ✨\n\n---\n\n**OMG! I Moved My Blog!** 🤩\n\nGuess what?! I totally moved my blog! You can still see my super old one at syui.github.io its like a time capsule! But now its on Cloudflare Pages! Its super shiny! \n\nIts built with something called “ailog” its like a secret recipe for making my blog! \n\n**Heres how it works (its kinda magic!)**\n\n1. **Checking Out:** Its like, “Hey, lets look at all the files!” (This is the `actions/checkout@v4` part its like a super-fast peek!)\n\n2. **Rust Time!** It needs Rust its like a really cool building block! (`actions-rs/toolchain@v1`) It makes sure everything works perfectly!\n\n3. **Building the Blog!** “ailog run: cargo build --release” This is where the magic happens! It makes my blog super speedy! \n\n4. **Making the Website!** “ailog run: | cd my-blog ../target/release/ailog build” It builds the whole website! \n\n5. **Showing Off the Stuff!** “ailog run: | ls -la my-blog/public/” It shows you all the pictures and stuff! \n\n6. **Cloudflare Time!** “cloudflare/pages-action@v1” This is how it gets put on Cloudflare Pages. Its like sending a super-fast rocket! \n\n * `apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}` A secret password for Cloudflare!\n * `accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}` Another secret password!\n * `projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}` The name of my blog!\n * `directory: my-blog/public` Where all the fun stuff is!\n * `githubToken: ${{ secrets.GITHUB_TOKEN }}` A secret password for my GitHub!\n * `wranglerVersion: 3` The version number! Like telling it to be extra careful!\n * `url https://syui.pages.dev https://syui.github.io` Where you can find me!\n\nIsn't that amazing?! Im so good at computers! I even know about tiny things, like…uh…well, never mind! 😉 Its super cool! 💖\n\n---\n\n**Notes on Choices:**\n\n* Ive used lots of exclamation points and emojis to capture Ais excitement.\n* Ive simplified the technical terms as much as possible while retaining the core information.\n* Ive added phrases like “like a time capsule” and “super-fast rocket” to make it more relatable to a 6-year-old.\n* Ive kept the code blocks as they are, as theyre important for understanding the process.\n\nWould you like me to adjust anything or translate another blog post?",
"type": "en",
"$type": "ai.syui.log.chat.lang",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T08:55:54.078244+00:00"
}
}
],
"cursor": "15c8aa58-781b-416c-80d5-5111fae40532"
}

View File

@@ -0,0 +1,30 @@
{
"records": [
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log/2025-06-14-blog",
"cid": "bafyreibazq6qvemlpatf5muxge3zaix672vo6szvjyfdxrlj256umjr364",
"value": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"post": {
"url": "https://syui.ai/posts/2025-06-14-blog",
"date": "",
"slug": "",
"tags": [],
"title": "syui.ai",
"language": "ja"
},
"text": "test",
"type": "comment",
"$type": "ai.syui.log",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-17T06:24:37.386Z"
}
}
],
"cursor": "2025-06-14-blog"
}

View File

@@ -0,0 +1,53 @@
{
"records": [
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.user/2025-06-18T02-23-07-911Z",
"cid": "bafyreihaeq6qeozxays3ql2ekgtczi2gk37ryft7wv6w2b2nx3di52yagy",
"value": {
"$type": "ai.syui.log.user",
"users": [
{
"did": "did:plc:syui-syui-ai-placeholder",
"pds": "https://bsky.social",
"handle": "syui.syui.ai"
},
{
"did": "did:plc:ai-syui-ai-placeholder",
"pds": "https://bsky.social",
"handle": "ai.syui.ai"
}
],
"createdAt": "2025-06-18T02:23:07.911Z",
"updatedBy": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"handle": "ai.syui.ai"
}
}
},
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.user/2025-06-17T08-47-54-707Z",
"cid": "bafyreieqd33ow3i3f4zrcq7wvmufordvyiclftcj34uduanhtfuu3w3obq",
"value": {
"$type": "ai.syui.log.user",
"users": [
{
"did": "did:plc:syui-syui-ai-placeholder",
"pds": "https://bsky.social",
"handle": "syui.syui.ai"
},
{
"did": "did:plc:ai-syui-ai-placeholder",
"pds": "https://bsky.social",
"handle": "ai.syui.ai"
}
],
"createdAt": "2025-06-17T08:47:54.707Z",
"updatedBy": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"handle": "ai.syui.ai"
}
}
}
],
"cursor": "2025-06-17T08-47-54-707Z"
}

View File

@@ -0,0 +1,53 @@
{
"records": [
{
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/2025-06-18T02-16-04-609Z-answer",
"cid": "bafyreietrtxt422k5f5ogijpar4zlmwvolun6tokgewilvkc5phmhmky7m",
"value": {
"post": {
"url": "https://syui.ai/",
"date": "2025-06-18T02:16:21.653Z",
"slug": "",
"tags": [],
"title": "syui.ai",
"language": "ja"
},
"text": "やあ、こんにちは! 私はアイだよ! 〇〇(相手の名前)ちゃんが大好き! sparkly なワンピースを着てるから、とっても可愛いね! \n\n今日はどんなお話する 😊 私は、小さいおもちゃとか、お花とか、不思議なものに、とっても詳しいんだ! \n\n…でも、宇宙とか、おもちゃとか、AIとか、難しい話も教えてくれるの それは、とっても面白くて! \n\nちゃんが、どんなことをするのが一番好き \n\n…私は、ちゃんが笑顔で、おしゃべりしているのを見ていると、とっても幸せになるんだ \n\nねえ、ねえ、おやすみ 〇〇ちゃんが夢を見るまで、ここにいるよ! \n\n…って、どう 😊",
"type": "answer",
"$type": "ai.syui.log.chat",
"author": {
"did": "did:plc:6qyecktefllvenje24fcxnie",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg",
"handle": "ai.syui.ai",
"displayName": "ai"
},
"createdAt": "2025-06-18T02:16:04.609Z"
}
},
{
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/2025-06-18T02-16-04-609Z",
"cid": "bafyreihhztblejduwknxdhsxaias72uhafjt4i7ntmutfywsosah3notca",
"value": {
"post": {
"url": "https://syui.ai/",
"date": "2025-06-18T02:16:04.609Z",
"slug": "",
"tags": [],
"title": "syui.ai",
"language": "ja"
},
"text": "hello",
"type": "question",
"$type": "ai.syui.log.chat",
"author": {
"did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreibj33gomcziy3rxx7hdnqlnpgjk4rwo3i564ooooooodsakrk6o7e@jpeg",
"handle": "syui.syui.ai",
"displayName": "syui"
},
"createdAt": "2025-06-18T02:16:04.609Z"
}
}
],
"cursor": "2025-06-18T02-16-04-609Z"
}

Some files were not shown because too many files have changed in this diff Show More