diff --git a/.gitea/workflows/cloudflare-pages.yml b/.gitea/workflows/cloudflare-pages.yml
new file mode 100644
index 0000000..a057fa2
--- /dev/null
+++ b/.gitea/workflows/cloudflare-pages.yml
@@ -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 }} } } }"
\ No newline at end of file
diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
new file mode 100644
index 0000000..30224a8
--- /dev/null
+++ b/.gitea/workflows/release.yml
@@ -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
\ No newline at end of file
diff --git a/.github/workflows/cloudflare-pages.yml b/.github/workflows/cloudflare-pages.yml
index 4449f6a..bf9116e 100644
--- a/.github/workflows/cloudflare-pages.yml
+++ b/.github/workflows/cloudflare-pages.yml
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
- OAUTH_DIR: oauth_new
+ OAUTH_DIR: oauth
KEEP_DEPLOYMENTS: 5
jobs:
@@ -110,48 +110,49 @@ jobs:
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
wranglerVersion: '3'
- cleanup:
- needs: deploy
- runs-on: ubuntu-latest
- if: success()
-
- steps:
- - name: Wait for deployment to complete
- run: sleep 3
-
- - name: Cleanup old deployments
- run: |
- # 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!"
+# 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!"
diff --git a/.gitignore b/.gitignore
index 30d935a..4d64041 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,6 @@ my-blog/static/index.html
my-blog/templates/oauth-assets.html
cloudflared-config.yml
.config
-oauth-server-example
atproto
+oauth_old
+oauth_example
diff --git a/bin/ailog-linux-x86_64.tar.gz b/bin/ailog-linux-x86_64.tar.gz
index dd0b295..f6baddb 100644
Binary files a/bin/ailog-linux-x86_64.tar.gz and b/bin/ailog-linux-x86_64.tar.gz differ
diff --git a/oauth_new/.env b/oauth/.env
similarity index 100%
rename from oauth_new/.env
rename to oauth/.env
diff --git a/oauth/.env.production b/oauth/.env.production
index 89e9ede..b6d20ee 100644
--- a/oauth/.env.production
+++ b/oauth/.env.production
@@ -1,21 +1,10 @@
-# Production environment variables
-VITE_APP_HOST=https://syui.ai
+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
-# Handle-based Configuration (DIDs resolved at runtime)
-VITE_ATPROTO_PDS=syu.is
-VITE_ADMIN_HANDLE=ai.syui.ai
-VITE_AI_HANDLE=ai.syui.ai
-VITE_OAUTH_COLLECTION=ai.syui.log
-VITE_ATPROTO_WEB_URL=https://bsky.app
-VITE_ATPROTO_HANDLE_LIST=["syui.syui.ai","ai.syui.ai","ai.ai"]
-
-# AI Configuration
-VITE_AI_ENABLED=true
-VITE_AI_ASK_AI=true
-VITE_AI_PROVIDER=ollama
-VITE_AI_MODEL=gemma3:1b
-VITE_AI_HOST=https://ollama.syui.ai
-VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
-
+# Production settings - Disable development features
+VITE_ENABLE_TEST_UI=false
+VITE_ENABLE_DEBUG=false
\ No newline at end of file
diff --git a/oauth_new/ASK_AI_INTEGRATION.md b/oauth/ASK_AI_INTEGRATION.md
similarity index 100%
rename from oauth_new/ASK_AI_INTEGRATION.md
rename to oauth/ASK_AI_INTEGRATION.md
diff --git a/oauth_new/AVATAR_SYSTEM.md b/oauth/AVATAR_SYSTEM.md
similarity index 100%
rename from oauth_new/AVATAR_SYSTEM.md
rename to oauth/AVATAR_SYSTEM.md
diff --git a/oauth_new/AVATAR_USAGE_EXAMPLES.md b/oauth/AVATAR_USAGE_EXAMPLES.md
similarity index 100%
rename from oauth_new/AVATAR_USAGE_EXAMPLES.md
rename to oauth/AVATAR_USAGE_EXAMPLES.md
diff --git a/oauth_new/CLOUDFLARE_DEPLOY.yml b/oauth/CLOUDFLARE_DEPLOY.yml
similarity index 100%
rename from oauth_new/CLOUDFLARE_DEPLOY.yml
rename to oauth/CLOUDFLARE_DEPLOY.yml
diff --git a/oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml b/oauth/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml
similarity index 100%
rename from oauth_new/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml
rename to oauth/CLOUDFLARE_DEPLOY_WITH_CLEANUP.yml
diff --git a/oauth_new/DEPLOYMENT.md b/oauth/DEPLOYMENT.md
similarity index 100%
rename from oauth_new/DEPLOYMENT.md
rename to oauth/DEPLOYMENT.md
diff --git a/oauth_new/DEVELOPMENT.md b/oauth/DEVELOPMENT.md
similarity index 100%
rename from oauth_new/DEVELOPMENT.md
rename to oauth/DEVELOPMENT.md
diff --git a/oauth_new/ENV_SETUP.md b/oauth/ENV_SETUP.md
similarity index 100%
rename from oauth_new/ENV_SETUP.md
rename to oauth/ENV_SETUP.md
diff --git a/oauth_new/IMPLEMENTATION_GUIDE.md b/oauth/IMPLEMENTATION_GUIDE.md
similarity index 100%
rename from oauth_new/IMPLEMENTATION_GUIDE.md
rename to oauth/IMPLEMENTATION_GUIDE.md
diff --git a/oauth_new/IMPROVEMENT_PLAN.md b/oauth/IMPROVEMENT_PLAN.md
similarity index 100%
rename from oauth_new/IMPROVEMENT_PLAN.md
rename to oauth/IMPROVEMENT_PLAN.md
diff --git a/oauth_new/OAUTH_FIX.md b/oauth/OAUTH_FIX.md
similarity index 100%
rename from oauth_new/OAUTH_FIX.md
rename to oauth/OAUTH_FIX.md
diff --git a/oauth_new/PHASE1_QUICK_FIXES.md b/oauth/PHASE1_QUICK_FIXES.md
similarity index 100%
rename from oauth_new/PHASE1_QUICK_FIXES.md
rename to oauth/PHASE1_QUICK_FIXES.md
diff --git a/oauth_new/PROGRESS.md b/oauth/PROGRESS.md
similarity index 100%
rename from oauth_new/PROGRESS.md
rename to oauth/PROGRESS.md
diff --git a/oauth_new/README.md b/oauth/README.md
similarity index 100%
rename from oauth_new/README.md
rename to oauth/README.md
diff --git a/oauth_new/build-minimal.js b/oauth/build-minimal.js
similarity index 100%
rename from oauth_new/build-minimal.js
rename to oauth/build-minimal.js
diff --git a/oauth_new/cleanup-deployments.yml b/oauth/cleanup-deployments.yml
similarity index 100%
rename from oauth_new/cleanup-deployments.yml
rename to oauth/cleanup-deployments.yml
diff --git a/oauth/index.html b/oauth/index.html
index 7652c5a..5664ef4 100644
--- a/oauth/index.html
+++ b/oauth/index.html
@@ -1,20 +1,11 @@
-
-
-
-
- ai.card
-
-
-
-
-
-
+
+
+
+ Comments Test
+
+
+
+
+
\ No newline at end of file
diff --git a/oauth_new/json/ai.syui.ai_chat_comment.json b/oauth/json/ai.syui.ai_chat_comment.json
similarity index 100%
rename from oauth_new/json/ai.syui.ai_chat_comment.json
rename to oauth/json/ai.syui.ai_chat_comment.json
diff --git a/oauth_new/json/ai.syui.ai_chat_lang.json b/oauth/json/ai.syui.ai_chat_lang.json
similarity index 100%
rename from oauth_new/json/ai.syui.ai_chat_lang.json
rename to oauth/json/ai.syui.ai_chat_lang.json
diff --git a/oauth_new/json/ai.syui.ai_log.json b/oauth/json/ai.syui.ai_log.json
similarity index 100%
rename from oauth_new/json/ai.syui.ai_log.json
rename to oauth/json/ai.syui.ai_log.json
diff --git a/oauth_new/json/ai.syui.ai_user.json b/oauth/json/ai.syui.ai_user.json
similarity index 100%
rename from oauth_new/json/ai.syui.ai_user.json
rename to oauth/json/ai.syui.ai_user.json
diff --git a/oauth_new/json/syui.syui.ai_chat.json b/oauth/json/syui.syui.ai_chat.json
similarity index 100%
rename from oauth_new/json/syui.syui.ai_chat.json
rename to oauth/json/syui.syui.ai_chat.json
diff --git a/oauth_new/json/syui.syui.ai_log.json b/oauth/json/syui.syui.ai_log.json
similarity index 100%
rename from oauth_new/json/syui.syui.ai_log.json
rename to oauth/json/syui.syui.ai_log.json
diff --git a/oauth/package.json b/oauth/package.json
index 320fa3c..65400e5 100644
--- a/oauth/package.json
+++ b/oauth/package.json
@@ -1,36 +1,22 @@
{
- "name": "aicard",
- "version": "0.1.1",
- "private": true,
+ "name": "ailog-oauth",
+ "version": "0.2.2",
+ "type": "module",
"scripts": {
- "dev": "vite --mode development",
- "build": "vite build --mode production",
- "build:dev": "vite build --mode development",
- "build:local": "VITE_APP_HOST=http://localhost:4173 vite build --mode development",
- "preview": "npm run test:console && vite preview",
- "test": "vitest",
- "test:console": "node -r esbuild-register src/tests/console-test.ts"
+ "dev": "vite",
+ "build": "vite build && node build-minimal.js",
+ "preview": "vite preview"
},
"dependencies": {
- "@atproto/api": "^0.15.12",
- "@atproto/did": "^0.1.5",
- "@atproto/identity": "^0.4.8",
- "@atproto/oauth-client-browser": "^0.3.19",
- "@atproto/xrpc": "^0.7.0",
- "axios": "^1.6.2",
- "framer-motion": "^10.16.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^7.6.1"
+ "@atproto/api": "^0.15.12",
+ "@atproto/oauth-client-browser": "^0.3.19"
},
"devDependencies": {
- "@types/react": "^18.2.45",
- "@types/react-dom": "^18.2.18",
- "@vitejs/plugin-react": "^4.2.1",
- "typescript": "^5.3.3",
- "vite": "^5.0.10",
- "vitest": "^1.1.0",
- "esbuild": "^0.19.10",
- "esbuild-register": "^3.5.0"
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@vitejs/plugin-react": "^4.0.0",
+ "vite": "^5.0.0"
}
}
diff --git a/oauth/public/.well-known/jwks.json b/oauth/public/.well-known/jwks.json
deleted file mode 100644
index d8a3f40..0000000
--- a/oauth/public/.well-known/jwks.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "keys": [
- {
- "kty": "EC",
- "crv": "P-256",
- "x": "mock_x_coordinate_base64url",
- "y": "mock_y_coordinate_base64url",
- "d": "mock_private_key_base64url",
- "use": "sig",
- "kid": "ai-card-oauth-key-1",
- "alg": "ES256"
- }
- ]
-}
\ No newline at end of file
diff --git a/oauth/public/client-metadata.json b/oauth/public/client-metadata.json
deleted file mode 100644
index 37a408d..0000000
--- a/oauth/public/client-metadata.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "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://syui.ai/oauth/callback",
- "https://syui.ai/"
- ],
- "response_types": [
- "code"
- ],
- "grant_types": [
- "authorization_code",
- "refresh_token"
- ],
- "token_endpoint_auth_method": "none",
- "scope": "atproto transition:generic",
- "subject_type": "public",
- "application_type": "web",
- "dpop_bound_access_tokens": true
-}
diff --git a/oauth/src/App.css b/oauth/src/App.css
index ec35d9a..4713471 100644
--- a/oauth/src/App.css
+++ b/oauth/src/App.css
@@ -1,360 +1,133 @@
-/* Theme Colors */
+/* Theme Colors - Match ailog style */
:root {
- --theme-color: #FF4500;
- --white: #fff;
- --light-gray: #aaa;
- --dark-gray: #666;
- --background: #fff;
+ --primary: #f40;
+ --primary-hover: #e03000;
+ --danger: #f91880;
+ --danger-hover: #d91a60;
+ --success: #00ba7c;
+ --warning: #ffad1f;
+ --text: #1f2328;
+ --text-secondary: #656d76;
+ --background: #ffffff;
+ --background-secondary: #f6f8fa;
+ --border: #d1d9e0;
+ --hover: rgba(15, 20, 25, 0.1);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: var(--background);
+ color: var(--text);
+ line-height: 1.6;
+ font-size: 16px;
}
.app {
min-height: 100vh;
- background: linear-gradient(180deg, #f8f9fa 0%, var(--background) 100%);
- color: var(--dark-gray);
+ background: var(--background);
}
-.app-header {
- text-align: center;
- padding: 40px 20px;
- border-bottom: 1px solid #e9ecef;
- position: relative;
+/* Header */
+.oauth-app-header {
+ background: var(--background);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ width: 100%;
}
-.app-nav {
- display: flex;
- justify-content: center;
- gap: 8px;
- padding: 20px;
- background: rgba(0, 0, 0, 0.02);
- border-bottom: 1px solid #e9ecef;
- margin-bottom: 40px;
-}
-
-.nav-button {
- padding: 12px 20px;
- border: 1px solid #dee2e6;
- border-radius: 8px;
- background: rgba(255, 255, 255, 0.8);
- color: #6c757d;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- backdrop-filter: blur(10px);
-}
-
-.nav-button:hover {
- background: rgba(102, 126, 234, 0.1);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- color: #495057;
-}
-
-.nav-button.active {
- background: var(--theme-color);
- color: var(--white);
- border: 1px solid var(--theme-color);
- box-shadow: 0 4px 16px rgba(255, 69, 0, 0.4);
-}
-
-.nav-button.active:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(255, 69, 0, 0.5);
-}
-
-.app-header h1 {
- font-size: 48px;
- margin: 0;
- background: linear-gradient(90deg, #fff700 0%, #ff00ff 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.app-header p {
- color: #6c757d;
- margin-top: 10px;
-}
-
-.user-info {
- position: absolute;
- top: 20px;
- right: 20px;
+.oauth-header-content {
display: flex;
+ justify-content: flex-start;
align-items: center;
- gap: 15px;
-}
-
-.user-handle {
- color: #495057;
- font-weight: bold;
- background: rgba(102, 126, 234, 0.1);
- padding: 6px 12px;
- border-radius: 20px;
- border: 1px solid #dee2e6;
-}
-
-.login-button,
-.logout-button,
-.backup-button,
-.token-button {
- padding: 8px 16px;
- border: none;
- border-radius: 8px;
- font-size: 12px;
- font-weight: bold;
- cursor: pointer;
- transition: all 0.3s ease;
- margin-left: 8px;
-}
-
-.login-button {
- background: var(--theme-color);
- color: var(--white);
- border: 1px solid var(--theme-color);
-}
-
-.backup-button {
- background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
- color: white;
- border: 1px solid #28a745;
-}
-
-.token-button {
- background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
- color: white;
- border: 1px solid #ffc107;
-}
-
-.logout-button {
- background: rgba(108, 117, 125, 0.1);
- color: #495057;
- border: 1px solid #dee2e6;
-}
-
-.login-button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
-}
-
-.backup-button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
-}
-
-.token-button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);
-}
-
-.logout-button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- background: rgba(108, 117, 125, 0.2);
-}
-
-.loading {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100vh;
- font-size: 24px;
- color: #667eea;
-}
-
-.app-main {
- max-width: 1000px;
- margin: 0 auto;
- padding: 40px 20px;
-}
-
-@media (max-width: 1000px) {
- * {
- max-width: 100% !important;
- box-sizing: border-box !important;
- }
-
- .app .app-main {
- max-width: 100% !important;
- margin: 0 !important;
- padding: 0px !important;
- }
-
- .comment-item {
- padding: 0px !important;
- margin: 0px !important;
- }
-
- .auth-section {
- padding: 0px !important;
- }
-
- .comments-list {
- padding: 0px !important;
- }
-
- .comment-section {
- padding: 30px 0 !important;
- margin: 0px !important;
- }
-
- .comment-content {
- padding: 10px !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- white-space: pre-wrap !important;
- }
-
- .comment-header {
- padding: 10px !important;
- }
-
- /* Fix overflow on article pages */
- article.article-content {
- overflow-x: hidden !important;
- }
-
- /* Ensure full width on mobile */
- .app {
- max-width: 100vw !important;
- overflow-x: hidden !important;
- }
-
- /* Fix button overflow */
- button {
- max-width: 100%;
- white-space: normal;
- }
-
- /* Fix comment-meta URI overflow */
- .comment-meta {
- word-break: break-all !important;
- overflow-wrap: break-word !important;
- }
-}
-
-.gacha-section {
- text-align: center;
- margin-bottom: 60px;
-}
-
-.gacha-section h2 {
- font-size: 32px;
- margin-bottom: 30px;
-}
-
-.gacha-buttons {
- display: flex;
- gap: 20px;
- justify-content: center;
- flex-wrap: wrap;
-}
-
-.gacha-button {
- padding: 20px 40px;
- font-size: 18px;
- font-weight: bold;
- border: none;
- border-radius: 12px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
-}
-
-.gacha-button:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
-}
-
-.gacha-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.gacha-button-premium {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- position: relative;
- overflow: hidden;
-}
-
-.gacha-button-premium::before {
- content: '';
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- background: linear-gradient(
- 45deg,
- transparent 30%,
- rgba(255, 255, 255, 0.2) 50%,
- transparent 70%
- );
- animation: shimmer 3s infinite;
-}
-
-.collection-section h2 {
- font-size: 32px;
- text-align: center;
- margin-bottom: 30px;
-}
-
-.card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 30px;
- justify-items: center;
-}
-
-.empty-message {
- text-align: center;
- color: #6c757d;
- font-size: 18px;
- margin-top: 40px;
-}
-
-.error {
- color: #ff4757;
- text-align: center;
- margin-top: 20px;
-}
-
-@keyframes shimmer {
- 0% { transform: translateX(-100%) rotate(45deg); }
- 100% { transform: translateX(100%) rotate(45deg); }
-}
-
-/* Comment System Styles */
-.comment-section {
max-width: 800px;
margin: 0 auto;
- /* padding: 20px; - removed to avoid double padding */
+ padding: 20px 0;
+ width: 100%;
}
-@media (max-width: 768px) {
- .comment-section {
- max-width: 100%;
- margin: 0;
- padding: 0;
- }
+.oauth-app-title {
+ font-size: 20px;
+ font-weight: 800;
+ color: var(--text);
}
+.oauth-header-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ width: 100%;
+}
-.auth-section {
- background: #f8f9fa;
- border: 1px solid #e9ecef;
+/* Buttons */
+.btn {
+ border: none;
border-radius: 8px;
- padding: 20px;
- margin-bottom: 20px;
- text-align: center;
+ font-weight: 700;
+ font-size: 15px;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: white;
+ padding: 8px 16px;
+}
+
+.btn-primary:hover {
+ background: var(--primary-hover);
+}
+
+.btn-danger {
+ background: var(--danger);
+ color: white;
+ padding: 8px 16px;
+}
+
+.btn-danger:hover {
+ background: var(--danger-hover);
+}
+
+.btn-outline {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 8px 16px;
+}
+
+.btn-outline:hover {
+ background: var(--hover);
+}
+
+.btn-sm {
+ padding: 4px 12px;
+ font-size: 13px;
+}
+
+/* Auth Section */
+.auth-section {
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
.auth-section.search-bar-layout {
display: flex;
align-items: center;
- padding: 10px;
- gap: 10px;
+ padding: 0;
+ gap: 0;
+ width: 100%;
}
.auth-section.search-bar-layout .handle-input {
@@ -362,627 +135,712 @@
margin: 0;
padding: 10px 15px;
font-size: 16px;
- border: 1px solid #dee2e6;
- border-radius: 6px 0 0 6px;
- background: white;
+ border: 1px solid var(--border);
+ border-radius: 8px 0 0 8px;
+ background: var(--background);
outline: none;
transition: border-color 0.2s;
+ width: 100%;
+ text-align: left;
+ color: var(--text);
}
.auth-section.search-bar-layout .handle-input:focus {
- border-color: var(--theme-color);
+ border-color: var(--primary);
}
-.auth-section.search-bar-layout .atproto-button {
+.auth-section.search-bar-layout .auth-button {
+ border-radius: 0 8px 8px 0;
+ border: 1px solid var(--primary);
+ border-left: none;
margin: 0;
- padding: 10px 20px;
- border-radius: 0 6px 6px 0;
- min-width: 50px;
- font-weight: bold;
- height: auto;
+ padding: 10px 15px;
}
-.atproto-button {
- background: var(--theme-color);
- color: var(--white);
+/* Auth Button */
+.auth-button {
+ background: var(--primary);
+ color: white;
border: none;
- padding: 12px 24px;
- border-radius: 6px;
- font-size: 16px;
- font-weight: bold;
+ border-radius: 8px;
+ padding: 8px 16px;
+ font-weight: 700;
cursor: pointer;
- margin-bottom: 15px;
- transition: all 0.3s ease;
+ transition: background 0.2s;
}
-.atproto-button:hover {
- filter: brightness(1.1);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
+.auth-button:hover {
+ background: var(--primary-hover);
}
-.username-input-section {
- margin: 15px 0;
+.auth-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
}
-.handle-input {
- width: 300px;
- max-width: 100%;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 6px;
- font-size: 14px;
- text-align: center;
+/* Main Content */
+.main-content {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px 0;
}
-/* Override for search bar layout */
-.search-bar-layout .handle-input {
- width: auto;
- text-align: left;
+.content-area {
+ background: var(--background);
}
-/* Mobile responsive for search bar */
-@media (max-width: 480px) {
- .auth-section.search-bar-layout {
- flex-direction: column;
- gap: 8px;
- }
-
- .auth-section.search-bar-layout .handle-input {
- width: 100%;
- border-radius: 6px;
- }
-
- .auth-section.search-bar-layout .atproto-button {
- width: 100%;
- border-radius: 6px;
- }
-}
-
-.auth-hint {
- color: #6c757d;
- font-size: 14px;
- margin: 10px 0 0 0;
-}
-
-.user-section {
- background: #e8f5e8;
- border: 1px solid #4caf50;
+/* Card Styles */
+.card {
+ background: var(--background);
+ border: 1px solid var(--border);
border-radius: 8px;
- padding: 20px;
- margin-bottom: 20px;
+ margin: 16px;
+ overflow: hidden;
}
-.user-section .user-info {
- position: static;
- display: block;
- margin-bottom: 20px;
+.card-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border);
+ font-weight: 700;
+ font-size: 20px;
}
-.user-profile {
- display: flex;
- align-items: center;
- gap: 15px;
- margin-bottom: 15px;
-}
-
-.user-avatar {
- width: 48px;
- height: 48px;
- border-radius: 50%;
- object-fit: cover;
- border: 2px solid #4caf50;
-}
-
-.user-details h3 {
- margin: 0 0 5px 0;
- color: #333;
- font-size: 18px;
-}
-
-.user-section .user-info h3 {
- margin: 0 0 10px 0;
- color: #333;
-}
-
-.user-section .user-handle {
- background: rgba(76, 175, 80, 0.1);
- color: #2e7d32;
- border: 1px solid #4caf50;
-}
-
-.user-section .user-did {
- font-family: monospace;
- font-size: 0.8em;
- color: #666;
- background: #f1f3f4;
- padding: 4px 8px;
- border-radius: 4px;
- margin-top: 5px;
- word-break: break-all;
+.card-content {
+ padding: 16px;
}
+/* Comment Form */
.comment-form {
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 20px;
- margin-bottom: 20px;
+ padding: 16px;
}
.comment-form h3 {
- margin: 0 0 15px 0;
- color: #333;
+ font-size: 20px;
+ font-weight: 800;
+ margin-bottom: 16px;
}
-.comment-form textarea {
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group label {
+ display: block;
+ font-weight: 700;
+ margin-bottom: 8px;
+ color: var(--text);
+}
+
+.form-input {
width: 100%;
padding: 12px;
- border: 1px solid #ddd;
- border-radius: 6px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 16px;
font-family: inherit;
- font-size: 14px;
- resize: vertical;
- box-sizing: border-box;
- min-height: 100px;
+ background: var(--background);
+ color: var(--text);
}
-.comment-form textarea:focus {
- border-color: #1185fe;
+.form-input:focus {
outline: none;
- box-shadow: 0 0 0 2px rgba(17, 133, 254, 0.1);
+ border-color: var(--primary);
+}
+
+.form-textarea {
+ min-height: 120px;
+ resize: vertical;
+ font-family: inherit;
}
.form-actions {
display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 10px;
+ justify-content: flex-end;
+ margin-top: 16px;
}
-.char-count {
- color: #666;
- font-size: 0.9em;
+/* Tab Navigation */
+.tab-header {
+ display: flex;
+ background: var(--background);
+ overflow-x: auto;
}
-.post-button {
- background: var(--theme-color);
- color: var(--white);
+.tab-btn {
+ background: none;
border: none;
- padding: 10px 20px;
- border-radius: 6px;
+ padding: 16px 20px;
+ font-size: 15px;
+ font-weight: 700;
+ color: var(--text-secondary);
cursor: pointer;
- font-size: 14px;
- font-weight: bold;
- transition: all 0.3s ease;
+ border-bottom: 2px solid transparent;
+ transition: color 0.2s;
+ white-space: nowrap;
}
-.post-button:hover:not(:disabled) {
- filter: brightness(1.1);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
+.tab-btn:hover {
+ color: var(--text);
+ background: var(--hover);
}
-.post-button:disabled {
- background: #6c757d;
- cursor: not-allowed;
- transform: none;
- box-shadow: none;
+.tab-btn.active {
+ color: var(--primary);
+ border-bottom-color: var(--primary);
}
-.comments-list {
- border-radius: 8px;
- padding: 0px;
+/* Record List */
+.record-item {
+ border-bottom: 1px solid var(--border);
+ padding: 16px;
+ transition: background 0.2s;
+ position: relative;
}
-.comments-header {
+.record-item:hover {
+ background: var(--background-secondary);
+}
+
+.record-header {
display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
+ align-items: flex-start;
+ gap: 12px;
+ margin-bottom: 12px;
}
-.comments-header h3 {
- margin: 0;
- color: #333;
-}
-
-.comments-controls {
- display: flex;
- gap: 10px;
-}
-
-.comments-toggle-button {
- background: var(--theme-color);
- color: var(--white);
- border: none;
- padding: 8px 16px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- font-weight: bold;
- transition: all 0.3s ease;
-}
-
-.comments-toggle-button:hover {
- filter: brightness(1.1);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
-}
-
-.comment-item {
- border: 1px solid #e9ecef;
- border-radius: 6px;
- padding: 15px;
- margin-bottom: 15px;
- background: #fff;
-}
-
-.comment-item:last-child {
- margin-bottom: 0;
-}
-
-.comment-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
-}
-
-.comment-avatar {
- width: 32px;
- height: 32px;
+.avatar {
+ width: 40px;
+ height: 40px;
border-radius: 50%;
object-fit: cover;
- border: 1px solid #ddd;
+ flex-shrink: 0;
}
-.comment-author-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
+.user-info {
flex: 1;
+ min-width: 0;
}
-.comment-author {
- font-weight: bold;
- color: #333;
- font-size: 0.95em;
+.display-name {
+ font-weight: 700;
+ color: var(--text);
+ font-size: 15px;
}
-.comment-handle {
- color: #666;
- font-size: 0.8em;
+.handle {
+ color: var(--text-secondary);
+ font-size: 15px;
}
-.comment-date {
- color: #666;
- font-size: 0.9em;
- margin-left: auto;
+.handle-link {
+ color: var(--text-secondary);
+ text-decoration: none;
}
-.delete-button {
- background: #dc3545;
- color: white;
- border: none;
- cursor: pointer;
- font-size: 12px;
- font-weight: 500;
- padding: 4px 8px;
- border-radius: 4px;
- transition: all 0.3s ease;
- margin-left: 8px;
+.handle-link:hover {
+ color: var(--primary);
+ text-decoration: underline;
}
-.delete-button:hover {
- background: #c82333;
- transform: scale(1.05);
+.timestamp {
+ color: var(--text-secondary);
+ font-size: 13px;
+ margin-top: 4px;
}
-.comment-content {
+.record-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.record-content {
+ font-size: 15px;
line-height: 1.5;
- color: #333;
- margin-bottom: 10px;
+ color: var(--text);
+ margin-bottom: 12px;
white-space: pre-wrap;
word-wrap: break-word;
}
-.comment-meta {
- padding: 8px;
- background: #f1f3f4;
- border-radius: 4px;
- font-size: 0.8em;
- color: #666;
-}
-
-.comment-meta small {
- font-family: monospace;
-}
-
-.no-comments {
- text-align: center;
- color: #666;
- font-style: italic;
- padding: 40px;
-}
-
-.error {
- background: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
- border-radius: 4px;
- padding: 10px;
- margin-top: 10px;
-}
-
-/* Admin Section Styles */
-.admin-section {
- background: #e3f2fd;
- border: 1px solid #2196f3;
- border-radius: 8px;
- padding: 20px;
- margin-top: 20px;
-}
-
-.admin-section h3 {
- margin: 0 0 15px 0;
- color: #1976d2;
- font-size: 16px;
-}
-
-.user-list-form {
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 15px;
-}
-
-.user-list-form textarea {
- width: 100%;
- padding: 12px;
- border: 1px solid #ddd;
- border-radius: 6px;
- font-family: inherit;
- font-size: 14px;
- resize: vertical;
- box-sizing: border-box;
- min-height: 80px;
-}
-
-.user-list-form textarea:focus {
- border-color: #2196f3;
- outline: none;
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
-}
-
-.admin-hint {
- color: #666;
- font-size: 0.9em;
- font-style: italic;
-}
-
-/* User List Records Styles */
-.user-list-records {
- margin-top: 20px;
-}
-
-.user-list-records h4 {
- margin: 0 0 15px 0;
- color: #1976d2;
- font-size: 14px;
-}
-
-.no-user-lists {
- text-align: center;
- color: #666;
- font-style: italic;
- padding: 20px;
-}
-
-.user-list-item {
- border: 1px solid #e3f2fd;
- border-radius: 6px;
- padding: 12px;
- margin-bottom: 10px;
- background: #fff;
-}
-
-.user-list-item:last-child {
- margin-bottom: 0;
-}
-
-.user-list-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
-}
-
-.user-list-actions {
+.record-meta {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 16px;
+ margin-top: 12px;
}
-.user-list-date {
- color: #666;
- font-size: 0.9em;
- font-weight: 500;
+.record-url {
+ color: var(--primary);
+ text-decoration: none;
+ font-size: 13px;
}
-.user-list-content {
- margin-top: 8px;
-}
-
-.user-handles {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 8px;
-}
-
-.user-handle-tag {
- background: #e3f2fd;
- color: #1976d2;
- padding: 4px 8px;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.pds-info {
- color: #666;
- font-size: 0.75em;
- font-weight: normal;
-}
-
-.user-list-meta {
- font-size: 0.8em;
- color: #666;
- background: #f8f9fa;
- padding: 6px 8px;
- border-radius: 4px;
- line-height: 1.4;
-}
-
-.user-list-meta small {
- font-family: monospace;
-}
-
-/* JSON Display Styles */
-.json-button {
- background: var(--theme-color);
- color: var(--white);
- border: none;
- padding: 4px 8px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- font-weight: 500;
- transition: all 0.3s ease;
-}
-
-.json-button:hover {
- filter: brightness(1.1);
- transform: scale(1.05);
+.record-url:hover {
+ text-decoration: underline;
}
+/* JSON Display */
.json-display {
margin-top: 12px;
- border: 1px solid #ddd;
- border-radius: 6px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
overflow: hidden;
}
-.json-display h5 {
- margin: 0;
+.json-header {
+ background: var(--background-secondary);
padding: 8px 12px;
- background: #f1f3f4;
- border-bottom: 1px solid #ddd;
- font-size: 0.9em;
- color: #333;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text-secondary);
}
.json-content {
- margin: 0;
- padding: 12px;
background: #f8f9fa;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 0.8em;
+ padding: 12px;
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 12px;
line-height: 1.4;
overflow-x: auto;
white-space: pre-wrap;
- word-break: break-word;
- color: #333;
- max-height: 400px;
+ max-height: 300px;
overflow-y: auto;
+ color: var(--text);
}
-/* Tab Navigation */
-.tab-navigation {
+/* Ask AI */
+.ask-ai-container {
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--background);
+}
+
+.ask-ai-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border);
+ background: var(--background-secondary);
display: flex;
- border-bottom: 2px solid #e1e5e9;
- margin-bottom: 20px;
+ justify-content: space-between;
+ align-items: center;
}
-.tab-button {
- background: none;
- border: none;
- padding: 12px 20px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 500;
- color: #656d76;
- border-bottom: 2px solid transparent;
- transition: all 0.2s;
+.ask-ai-header h3 {
+ font-size: 20px;
+ font-weight: 800;
}
-.tab-button:hover {
- color: var(--theme-color);
- background: #f6f8fa;
+.chat-container {
+ height: 400px;
+ overflow-y: auto;
+ padding: 16px;
}
-.tab-button.active {
- color: var(--theme-color);
- border-bottom-color: var(--theme-color);
- background: #f6f8fa;
+.chat-message {
+ margin-bottom: 16px;
}
+.user-message {
+ margin-left: 40px;
+}
-.chat-actions {
+.ai-message {
+ margin-right: 40px;
+}
+
+.message-header {
display: flex;
align-items: center;
gap: 8px;
-}
-
-.chat-type-button {
- background: var(--theme-color);
- color: var(--white);
- border: none;
- padding: 4px 8px;
- border-radius: 4px;
- cursor: default;
- font-size: 12px;
- font-weight: 500;
- margin-left: 4px;
-}
-
-.chat-type-text {
- font-size: 16px;
- margin-left: 4px;
-}
-
-
-.chat-date {
- color: #656d76;
- font-size: 12px;
-}
-
-.chat-content {
- background: #f6f8fa;
- padding: 12px;
- border-radius: 6px;
- border-left: 4px solid #d1d9e0;
margin-bottom: 8px;
- white-space: pre-wrap;
- line-height: 1.5;
}
-.chat-meta {
- font-size: 11px;
- color: #656d76;
+.message-content {
+ background: var(--background-secondary);
+ padding: 12px 16px;
+ border-radius: 8px;
+ font-size: 15px;
+ line-height: 1.4;
}
-.no-chat {
+.user-message .message-content {
+ background: var(--primary);
+ color: white;
+}
+
+.question-form {
+ padding: 16px;
+ border-top: 1px solid var(--border);
+ background: var(--background);
+}
+
+.input-container {
+ display: flex;
+ gap: 8px;
+ align-items: flex-end;
+}
+
+.question-input {
+ flex: 1;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px 16px;
+ font-size: 16px;
+ resize: none;
+ font-family: inherit;
+ background: var(--background);
+}
+
+.question-input:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.send-btn {
+ background: var(--primary);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ width: 36px;
+ height: 36px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+}
+
+.send-btn:hover:not(:disabled) {
+ background: var(--primary-hover);
+}
+
+.send-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Test UI */
+.test-ui {
+ border: 2px solid var(--danger);
+ border-radius: 8px;
+ margin: 16px;
+ background: #fff5f7;
+}
+
+.test-ui h2 {
+ color: var(--danger);
+ padding: 16px;
+ border-bottom: 1px solid var(--border);
+ margin: 0;
+}
+
+.test-ui .card-content {
+ padding: 16px;
+}
+
+/* Loading Skeleton */
+.loading-skeleton {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
+.skeleton-line {
+ background: var(--background-secondary);
+ border-radius: 4px;
+ margin-bottom: 8px;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+/* Error States */
+.error-message {
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ color: #991b1b;
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin: 16px 0;
+}
+
+.success-message {
+ background: #f0fdf4;
+ border: 1px solid #bbf7d0;
+ color: #166534;
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin: 16px 0;
+}
+
+/* Auth Notice */
+.auth-notice {
text-align: center;
- padding: 40px 20px;
- color: #656d76;
- font-style: italic;
+ color: var(--text-secondary);
+ font-size: 14px;
+ margin-top: 8px;
}
-.chat-message.comment-style {
- border-left: 4px solid var(--theme-color);
+/* Page Info */
+.page-info {
+ padding: 8px 16px;
+ background: var(--background-secondary);
+ font-size: 12px;
+ color: var(--text-secondary);
+ text-align: center;
}
+
+.bottom-actions {
+ padding: 20px;
+ text-align: center;
+ margin-top: 20px;
+}
+
+.test-section {
+ margin-top: 20px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .main-content {
+ max-width: 100%;
+ }
+
+ .content-area {
+ border-left: none;
+ border-right: none;
+ }
+
+ .card {
+ margin: 0;
+ border-radius: 0;
+ border-left: none;
+ border-right: none;
+ }
+
+ .app-header {
+ padding: 8px 16px;
+ }
+
+ .header-actions {
+ gap: 4px;
+ }
+
+ .btn {
+ padding: 6px 12px;
+ font-size: 14px;
+ }
+
+ .tab-btn {
+ padding: 12px 16px;
+ font-size: 14px;
+ }
+
+ .record-item {
+ padding: 12px 16px;
+ }
+
+ .chat-container {
+ height: 300px;
+ }
+}
+
+/* Avatar Styles */
+.avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+ border: 1px solid var(--border);
+}
+
+.avatar-loading {
+ background: var(--background-secondary);
+ border-radius: 50%;
+ position: relative;
+ overflow: hidden;
+}
+
+.avatar-loading::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
+ animation: loading-shimmer 1.5s infinite;
+}
+
+@keyframes loading-shimmer {
+ 0% { left: -100%; }
+ 100% { left: 100%; }
+}
+
+.avatar-fallback {
+ background: var(--background-secondary);
+ color: var(--text-secondary);
+ font-weight: 600;
+ border: 1px solid var(--border);
+}
+
+/* Avatar with Card */
+.avatar-container {
+ position: relative;
+ display: inline-block;
+}
+
+.avatar-card {
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--background);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ min-width: 200px;
+ margin-top: 8px;
+}
+
+.avatar-card::before {
+ content: '';
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-bottom: 8px solid var(--border);
+}
+
+.avatar-card::after {
+ content: '';
+ position: absolute;
+ top: -7px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid var(--background);
+}
+
+.avatar-card-image {
+ display: block;
+ margin: 0 auto 12px;
+}
+
+.avatar-card-info {
+ text-align: center;
+}
+
+.avatar-card-name {
+ font-weight: 700;
+ font-size: 16px;
+ margin-bottom: 4px;
+ color: var(--text);
+}
+
+.avatar-card-handle {
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 14px;
+}
+
+.avatar-card-handle:hover {
+ color: var(--primary);
+ text-decoration: underline;
+}
+
+/* Avatar List */
+.avatar-list {
+ display: flex;
+ align-items: center;
+}
+
+.avatar-list-item {
+ border: 2px solid var(--background);
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.avatar-list-more {
+ border: 2px solid var(--background);
+ font-weight: 600;
+ font-size: 12px;
+}
+
+/* Avatar Test Styles */
+.avatar-test-container {
+ margin: 16px;
+}
+
+.test-section {
+ margin-bottom: 32px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid var(--border);
+}
+
+.test-section:last-child {
+ border-bottom: none;
+}
+
+.test-section h3 {
+ margin-bottom: 16px;
+ color: var(--text);
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.avatar-examples {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.avatar-example {
+ text-align: center;
+}
+
+.avatar-example h4 {
+ margin-bottom: 8px;
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+.test-controls {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+/* Utilities */
+.hidden {
+ display: none;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
\ No newline at end of file
diff --git a/oauth_new/src/App.jsx b/oauth/src/App.jsx
similarity index 99%
rename from oauth_new/src/App.jsx
rename to oauth/src/App.jsx
index 9ab8d20..5e49c65 100644
--- a/oauth_new/src/App.jsx
+++ b/oauth/src/App.jsx
@@ -133,6 +133,7 @@ export default function App() {
{
refreshAdminData?.()
refreshUserData?.()
diff --git a/oauth/src/App.tsx b/oauth/src/App.tsx
deleted file mode 100644
index ee70db4..0000000
--- a/oauth/src/App.tsx
+++ /dev/null
@@ -1,1622 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { OAuthCallback } from './components/OAuthCallback';
-import { AIChat } from './components/AIChat';
-import { authService, User } from './services/auth';
-import { atprotoOAuthService } from './services/atproto-oauth';
-import { appConfig, getCollectionNames } from './config/app';
-import { getProfileForUser, detectPdsFromHandle, getApiUrlForUser, verifyPdsDetection, getNetworkConfigFromPdsEndpoint, getNetworkConfig } from './utils/pds-detection';
-import { isValidDid } from './utils/validation';
-import './App.css';
-
-function App() {
- // Handle OAuth callback detection
- if (window.location.search.includes('code=') || window.location.search.includes('state=')) {
- const urlInfo = `OAuth callback detected!\n\nURL: ${window.location.href}\nSearch: ${window.location.search}`;
- alert(urlInfo);
- }
-
- const [user, setUser] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [comments, setComments] = useState([]);
- const [commentText, setCommentText] = useState('');
- const [isPosting, setIsPosting] = useState(false);
- const [error, setError] = useState(null);
- const [handleInput, setHandleInput] = useState('');
- const [userListInput, setUserListInput] = useState('');
- const [isPostingUserList, setIsPostingUserList] = useState(false);
- const [userListRecords, setUserListRecords] = useState([]);
- const [showJsonFor, setShowJsonFor] = useState(null);
- const [activeTab, setActiveTab] = useState<'comments' | 'ai-chat' | 'lang-en' | 'ai-comment'>('comments');
- const [aiChatHistory, setAiChatHistory] = useState([]);
- const [langEnRecords, setLangEnRecords] = useState([]);
- const [aiCommentRecords, setAiCommentRecords] = useState([]);
- const [aiProfile, setAiProfile] = useState(null);
-
- const [adminDid, setAdminDid] = useState(null);
- const [aiDid, setAiDid] = useState(null);
-
- // ハンドルからDIDを解決する関数
- const resolveHandleToDid = async (handle: string): Promise => {
- try {
- const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(handle));
- return profile?.did || null;
- } catch {
- return null;
- }
- };
-
- useEffect(() => {
- // 管理者とAIのDIDを解決
- const resolveAdminAndAiDids = async () => {
- const [resolvedAdminDid, resolvedAiDid] = await Promise.all([
- resolveHandleToDid(appConfig.adminHandle),
- resolveHandleToDid(appConfig.aiHandle)
- ]);
-
- setAdminDid(resolvedAdminDid || appConfig.adminDid);
- setAiDid(resolvedAiDid || appConfig.aiDid);
- };
-
- resolveAdminAndAiDids();
-
- // Setup Jetstream WebSocket for real-time comments (optional)
- const setupJetstream = () => {
- try {
- const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe');
-
- const collections = getCollectionNames(appConfig.collections.base);
- ws.onopen = () => {
- ws.send(JSON.stringify({
- wantedCollections: [collections.comment]
- }));
- };
-
- ws.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- if (data.collection === collections.comment && data.commit?.operation === 'create') {
- // Optionally reload comments
- // loadAllComments(window.location.href);
- }
- } catch (err) {
- // Ignore parsing errors
- }
- };
-
- ws.onerror = (err) => {
- // Ignore Jetstream errors
- };
-
- return ws;
- } catch (err) {
- return null;
- }
- };
-
- // Jetstream + Cache example (disabled for now)
- // const jetstream = setupJetstream();
-
- // キャッシュからコメント読み込み
- const loadCachedComments = () => {
- const cached = localStorage.getItem('cached_comments_' + window.location.pathname);
- if (cached) {
- const { comments: cachedComments, timestamp } = JSON.parse(cached);
- // 5分以内のキャッシュなら使用
- if (Date.now() - timestamp < 5 * 60 * 1000) {
- setComments(cachedComments);
- return true;
- }
- }
- return false;
- };
-
- // DID解決が完了してからコメントとチャット履歴を読み込む
- const loadDataAfterDidResolution = () => {
- // キャッシュがなければ、ATProtoから取得(認証状態に関係なく)
- if (!loadCachedComments()) {
- loadAllComments(); // URLフィルタリングを無効にして全コメント表示
- }
-
- // Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
- loadAiChatHistory();
-
- // Load AI generated content (lang:en and AI comments)
- loadAIGeneratedContent();
- };
-
- // Load data immediately with fallback DIDs (skip DID resolution wait)
- loadDataAfterDidResolution();
-
- // Load AI profile from handle
- const loadAiProfile = async () => {
- try {
- // Use VITE_AI_HANDLE to detect PDS and get profile
- const handle = appConfig.aiHandle;
- if (!handle) {
- throw new Error('No AI handle configured');
- }
-
- // Detect PDS: Use VITE_ATPROTO_PDS if handle matches admin/ai handles
- let pds;
- if (handle === appConfig.adminHandle || handle === appConfig.aiHandle) {
- // Use configured PDS for admin/ai handles
- pds = appConfig.atprotoPds || 'syu.is';
- } else {
- // Use handle-based detection for other handles
- pds = detectPdsFromHandle(handle);
- }
-
- const config = getNetworkConfigFromPdsEndpoint(`https://${pds}`);
- const apiEndpoint = config.bskyApi;
-
-
- // Get profile from appropriate bsky API
- const profileResponse = await fetch(`${apiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
- if (profileResponse.ok) {
- const profileData = await profileResponse.json();
- setAiProfile({
- did: profileData.did || appConfig.aiDid,
- handle: profileData.handle || handle,
- displayName: profileData.displayName || appConfig.aiDisplayName || 'ai',
- avatar: profileData.avatar || generatePlaceholderAvatar(handle),
- description: profileData.description || appConfig.aiDescription || ''
- });
- } else {
- // Fallback to config values
- setAiProfile({
- did: appConfig.aiDid,
- handle: handle,
- displayName: appConfig.aiDisplayName || 'ai',
- avatar: generatePlaceholderAvatar(handle),
- description: appConfig.aiDescription || ''
- });
- }
- } catch (err) {
- // Failed to load AI profile
- // Fallback to config values
- setAiProfile({
- did: appConfig.aiDid,
- handle: appConfig.aiHandle,
- displayName: appConfig.aiDisplayName || 'ai',
- avatar: generatePlaceholderAvatar(appConfig.aiHandle || 'ai'),
- description: appConfig.aiDescription || ''
- });
- }
- };
- loadAiProfile();
-
- // Handle popstate events for mock OAuth flow
- const handlePopState = () => {
- const urlParams = new URLSearchParams(window.location.search);
- const isOAuthCallback = urlParams.has('code') && urlParams.has('state');
-
- if (isOAuthCallback) {
- // Force re-render to handle OAuth callback
- window.location.reload();
- }
- };
-
- window.addEventListener('popstate', handlePopState);
-
- // Check if this is an OAuth callback
- const urlParams = new URLSearchParams(window.location.search);
- const isOAuthCallback = urlParams.has('code') && urlParams.has('state');
-
- if (isOAuthCallback) {
- return; // Let OAuthCallback component handle this
- }
-
- // Check existing sessions
- const checkAuth = async () => {
- // First check OAuth session using official BrowserOAuthClient
- const oauthResult = await atprotoOAuthService.checkSession();
-
- if (oauthResult) {
- // Ensure handle is not DID
- const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle;
-
- // Note: appConfig.allowedHandles is used for PDS detection, not access control
-
- // Get user profile including avatar
- const userProfile = await getUserProfile(oauthResult.did, handle);
- setUser(userProfile);
-
- // Load all comments for display (this will be the default view)
- // Temporarily disable URL filtering to see all comments
- loadAllComments();
-
- // Load AI chat history
- loadAiChatHistory();
-
- // Load user list records if admin
- if (userProfile.did === adminDid) {
- loadUserListRecords();
- }
-
- setIsLoading(false);
- return;
- }
-
- // Fallback to legacy auth
- const verifiedUser = await authService.verify();
- if (verifiedUser) {
- // Check if handle is allowed
- if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(verifiedUser.handle)) {
- // Handle not in allowed list
- setError(`Access denied: ${verifiedUser.handle} is not authorized for this application.`);
- setIsLoading(false);
- return;
- }
-
- setUser(verifiedUser);
-
- // Load all comments for display (this will be the default view)
- // Temporarily disable URL filtering to see all comments
- loadAllComments();
-
- // Load user list records if admin
- if (verifiedUser.did === adminDid) {
- loadUserListRecords();
- }
- }
- setIsLoading(false);
-
- // 認証状態に関係なく、コメントを読み込む
- loadAllComments();
- };
-
- checkAuth();
-
- // Load AI generated content (public)
- loadAIGeneratedContent();
-
- return () => {
- window.removeEventListener('popstate', handlePopState);
- };
- }, []);
-
- // DID解決完了時にデータを再読み込み
- useEffect(() => {
- if (adminDid && aiDid) {
- loadAllComments();
- loadAiChatHistory();
- loadAIGeneratedContent();
- }
- }, [adminDid, aiDid]);
-
- const getUserProfile = async (did: string, handle: string): Promise => {
- try {
- const agent = atprotoOAuthService.getAgent();
- if (agent) {
- const profile = await agent.getProfile({ actor: handle });
- return {
- did: did,
- handle: handle,
- avatar: profile.data.avatar,
- displayName: profile.data.displayName || handle
- };
- }
- } catch (error) {
- // Failed to get user profile
- }
-
- // Fallback to basic user info
- return {
- did: did,
- handle: handle,
- avatar: generatePlaceholderAvatar(handle),
- displayName: handle
- };
- };
-
- const generatePlaceholderAvatar = (handle: string): string => {
- const initial = handle ? handle.charAt(0).toUpperCase() : 'U';
- const svg = ``;
- return `data:image/svg+xml;base64,${btoa(svg)}`;
- };
-
- const loadAiChatHistory = async () => {
- try {
- // Load all chat records from users in admin's user list
- const currentAdminDid = adminDid || appConfig.adminDid;
-
- // Use fallback DID if resolution failed
- if (!currentAdminDid) {
- return;
- }
-
- // Resolve admin's actual PDS from their DID
- let adminPdsEndpoint;
- try {
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- adminPdsEndpoint = config.pdsApi;
- } catch {
- // Fallback to configured PDS
- const adminConfig = getNetworkConfig(appConfig.atprotoPds);
- adminPdsEndpoint = adminConfig.pdsApi;
- }
-
- const collections = getCollectionNames(appConfig.collections.base);
-
- const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
-
- if (!userListResponse.ok) {
- setAiChatHistory([]);
- return;
- }
-
- const userListData = await userListResponse.json();
- const userRecords = userListData.records || [];
-
- // Extract unique DIDs from user records (including admin DID for their own chats)
- const allUserDids = [];
- userRecords.forEach(record => {
- if (record.value.users && Array.isArray(record.value.users)) {
- record.value.users.forEach(user => {
- if (user.did) {
- allUserDids.push(user.did);
- }
- });
- }
- });
-
- // Always include admin DID to check admin's own chats
- allUserDids.push(currentAdminDid);
-
- const userDids = [...new Set(allUserDids)];
-
- // Load chat records from all registered users (including admin) using per-user PDS detection
- const allChatRecords = [];
- for (const userDid of userDids) {
- try {
- // Use per-user PDS detection for each user's chat records
- let userPdsEndpoint;
- try {
- // Validate DID format before making API calls
- if (!userDid || !userDid.startsWith('did:')) {
- continue;
- }
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(userDid));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- userPdsEndpoint = config.pdsApi;
- } catch {
- userPdsEndpoint = atprotoApi; // Fallback
- }
-
- const chatResponse = await fetch(`${userPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collections.chat)}&limit=100`);
-
- if (chatResponse.ok) {
- const chatData = await chatResponse.json();
- const records = chatData.records || [];
- allChatRecords.push(...records);
- } else if (chatResponse.status === 400) {
- // Skip 400 errors (repo not found, etc)
- continue;
- }
- } catch (err) {
- continue;
- }
- }
-
- // Filter for page-specific content if on a post page
- let filteredRecords = allChatRecords;
- if (appConfig.rkey) {
- // On post page: show only chats for this specific post
- filteredRecords = allChatRecords.filter(record => {
- const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
- return recordRkey === appConfig.rkey;
- });
- } else {
- // On top page: show latest 3 records from all pages
- filteredRecords = allChatRecords.slice(0, 3);
- }
-
- // Filter out old records with invalid AI profile data (temporary fix for migration)
- const validRecords = filteredRecords.filter(record => {
- if (record.value.type === 'answer') {
- // This is an AI answer - check if it has valid AI profile
- return record.value.author?.handle &&
- record.value.author?.handle !== 'ai-assistant' &&
- record.value.author?.displayName !== 'AI Assistant';
- }
- return true; // Keep all questions
- });
-
- // Sort by creation time
- const sortedRecords = validRecords.sort((a, b) =>
- new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime()
- );
-
- setAiChatHistory(sortedRecords);
- } catch (err) {
- setAiChatHistory([]);
- }
- };
-
- // Load AI generated content from admin DID
- const loadAIGeneratedContent = async () => {
- try {
- const currentAdminDid = adminDid || appConfig.adminDid;
-
- // Use fallback DID if resolution failed
- if (!currentAdminDid) {
- return;
- }
-
- // Resolve admin's actual PDS from their DID
- let atprotoApi;
- try {
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- atprotoApi = config.pdsApi;
- } catch {
- // Fallback to configured PDS
- const adminConfig = getNetworkConfig(appConfig.atprotoPds);
- atprotoApi = adminConfig.pdsApi;
- }
- const collections = getCollectionNames(appConfig.collections.base);
-
- // Load lang:en records
- const langResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`);
- if (langResponse.ok) {
- const langData = await langResponse.json();
- const langRecords = langData.records || [];
-
- // Filter by current page rkey if on post page
- const filteredLangRecords = appConfig.rkey
- ? langRecords.filter(record => {
- // Compare rkey only (last part of path)
- const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
- return recordRkey === appConfig.rkey;
- })
- : langRecords.slice(0, 3); // Top page: latest 3
-
- setLangEnRecords(filteredLangRecords);
- }
-
- // Load AI comment records from admin account (not AI account)
- if (!currentAdminDid) {
- console.warn('No Admin DID available, skipping AI comment loading');
- setAiCommentRecords([]);
- return;
- }
-
- const commentResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`);
- if (commentResponse.ok) {
- const commentData = await commentResponse.json();
- const commentRecords = commentData.records || [];
-
- // Filter by current page rkey if on post page
- const filteredCommentRecords = appConfig.rkey
- ? commentRecords.filter(record => {
- // Compare rkey only (last part of path)
- const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
- return recordRkey === appConfig.rkey;
- })
- : commentRecords.slice(0, 3); // Top page: latest 3
-
- setAiCommentRecords(filteredCommentRecords);
- }
- } catch (err) {
- // Ignore errors
- }
- };
-
- const loadUserComments = async (did: string) => {
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) {
- return;
- }
-
- // Get comments from current user
- const response = await agent.api.com.atproto.repo.listRecords({
- repo: did,
- collection: getCollectionNames(appConfig.collections.base).comment,
- limit: 100,
- });
- const userComments = response.data.records || [];
-
- // Enhance comments with fresh profile information
- const enhancedComments = await Promise.all(
- userComments.map(async (record) => {
- if (record.value.author?.handle) {
- try {
- // Use existing PDS detection logic
- const handle = record.value.author.handle;
- const pds = detectPdsFromHandle(handle);
- const config = getNetworkConfigFromPdsEndpoint(`https://${pds}`);
- const apiEndpoint = config.bskyApi;
-
- const profileResponse = await fetch(`${apiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
- if (profileResponse.ok) {
- const profileData = await profileResponse.json();
-
- // Determine correct web URL based on avatar source
- let webUrl = config.webUrl; // Default from handle-based detection
- if (profileData.avatar && profileData.avatar.includes('cdn.bsky.app')) {
- webUrl = 'https://bsky.app'; // Override to Bluesky if avatar is from Bluesky
- }
-
- return {
- ...record,
- value: {
- ...record.value,
- author: {
- ...record.value.author,
- avatar: profileData.avatar,
- displayName: profileData.displayName || handle,
- _pdsEndpoint: `https://${pds}`, // Store PDS info for later use
- _webUrl: webUrl, // Store corrected web URL for profile links
- }
- }
- };
- } else {
- // If profile fetch fails, still add PDS info for links
- return {
- ...record,
- value: {
- ...record.value,
- author: {
- ...record.value.author,
- _pdsEndpoint: `https://${pds}`,
- _webUrl: config.webUrl,
- }
- }
- };
- }
- } catch (err) {
- // Ignore enhancement errors, use existing data
- }
- }
- return record;
- })
- );
-
- setComments(enhancedComments);
- } catch (err) {
- // Ignore load errors
- setComments([]);
- }
- };
-
- // JSONからユーザーリストを取得
- const loadUsersFromRecord = async () => {
- try {
- // 管理者のユーザーリストを取得 using proper PDS detection
- const currentAdminDid = adminDid || appConfig.adminDid;
-
- // Use per-user PDS detection for admin's records
- let adminPdsEndpoint;
- try {
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- adminPdsEndpoint = config.pdsApi;
- } catch {
- adminPdsEndpoint = 'https://bsky.social'; // Fallback
- }
-
- const userCollectionUrl = `${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`;
-
- const response = await fetch(userCollectionUrl);
-
-
- if (!response.ok) {
- return getDefaultUsers();
- }
-
- const data = await response.json();
- const userRecords = data.records || [];
-
- if (userRecords.length === 0) {
- const defaultUsers = getDefaultUsers();
- return defaultUsers;
- }
-
- // レコードからユーザーリストを構築し、プレースホルダーDIDを実際のDIDに解決
- const allUsers = [];
- for (const record of userRecords) {
- if (record.value.users) {
- // プレースホルダーDIDを実際のDIDに解決
- const resolvedUsers = await Promise.all(
- record.value.users.map(async (user) => {
- if (user.did && user.did.includes('-placeholder')) {
- // Resolving placeholder DID using proper PDS detection
- try {
- const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(user.handle));
- if (profile && profile.did) {
- // Resolved DID
- return {
- ...user,
- did: profile.did
- };
- }
- } catch (err) {
- // Failed to resolve DID
- }
- }
- return user;
- })
- );
- allUsers.push(...resolvedUsers);
- }
- }
-
- return allUsers;
- } catch (err) {
- // Failed to load users from records, using defaults
- return getDefaultUsers();
- }
- };
-
- // ユーザーリスト一覧を読み込み
- const loadUserListRecords = async () => {
- try {
- // Loading user list records using proper PDS detection
- const currentAdminDid = adminDid || appConfig.adminDid;
-
- // Use per-user PDS detection for admin's records
- let adminPdsEndpoint;
- try {
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- adminPdsEndpoint = config.pdsApi;
- } catch {
- adminPdsEndpoint = 'https://bsky.social'; // Fallback
- }
-
- const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
-
- if (!response.ok) {
- // Failed to fetch user list records
- setUserListRecords([]);
- return;
- }
-
- const data = await response.json();
- const records = data.records || [];
-
- // 新しい順にソート
- const sortedRecords = records.sort((a, b) =>
- new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime()
- );
-
- // Loaded user list records
- setUserListRecords(sortedRecords);
- } catch (err) {
- // Failed to load user list records
- setUserListRecords([]);
- }
- };
-
- const getDefaultUsers = () => {
- const currentAdminDid = adminDid || appConfig.adminDid;
- const defaultUsers = [
- // Default admin user
- { did: currentAdminDid, handle: appConfig.adminHandle, pds: 'https://syu.is' },
- ];
-
- // 現在ログインしているユーザーも追加(重複チェック)
- if (user && user.did && user.handle && !defaultUsers.find(u => u.did === user.did)) {
- // Detect PDS based on handle
- const userPds = user.handle.endsWith('.syu.is') ? 'https://syu.is' :
- user.handle.endsWith('.syui.ai') ? 'https://syu.is' :
- 'https://bsky.social';
-
- defaultUsers.push({
- did: user.did,
- handle: user.handle,
- pds: userPds
- });
- }
-
- return defaultUsers;
- };
-
- // 新しい関数: 全ユーザーからコメントを収集
- const loadAllComments = async (pageUrl?: string) => {
- try {
-
- // ユーザーリストを動的に取得
- const knownUsers = await loadUsersFromRecord();
-
- const allComments = [];
-
- // 各ユーザーからコメントを収集
- for (const user of knownUsers) {
- try {
-
- // Use per-user PDS detection for repo operations
- let pdsEndpoint;
- try {
- const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(user.did));
- const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
- pdsEndpoint = config.pdsApi;
- } catch {
- // Fallback to user.pds if PDS detection fails
- pdsEndpoint = user.pds;
- }
-
- const collections = getCollectionNames(appConfig.collections.base);
- const response = await fetch(`${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`);
-
- if (!response.ok) {
- continue;
- }
-
- const data = await response.json();
- const userRecords = data.records || [];
-
- // Flatten comments from new array format
- const userComments = [];
- for (const record of userRecords) {
- if (record.value.comments && Array.isArray(record.value.comments)) {
- // New format: array of comments
- for (const comment of record.value.comments) {
- userComments.push({
- ...record,
- value: comment,
- originalRecord: record // Keep reference to original record
- });
- }
- } else if (record.value.text) {
- // Old format: single comment
- userComments.push(record);
- }
- }
-
-
- // ページpathでフィルタリング(指定された場合)
- const filteredComments = pageUrl && appConfig.rkey
- ? userComments.filter(record => {
- try {
- // Compare rkey only (last part of path)
- const recordRkey = record.value.url ? new URL(record.value.url).pathname.split('/').pop() : '';
- return recordRkey === appConfig.rkey;
- } catch (err) {
- return false;
- }
- })
- : userComments;
-
- allComments.push(...filteredComments);
- } catch (err) {
- }
- }
-
- // 時間順にソート(新しい順)
- const sortedComments = allComments.sort((a, b) =>
- new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime()
- );
-
- // プロフィール情報で拡張(認証なしでも取得可能)
- const enhancedComments = await Promise.all(
- sortedComments.map(async (record) => {
- if (!record.value.author?.avatar && record.value.author?.handle) {
- try {
- // Use per-user PDS detection for profile fetching
- const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(record.value.author.handle));
-
- if (profile) {
- // Determine network config based on profile data
- let webUrl = 'https://bsky.app'; // Default to Bluesky
- if (profile.avatar && profile.avatar.includes('cdn.bsky.app')) {
- webUrl = 'https://bsky.app';
- } else if (profile.avatar && profile.avatar.includes('bsky.syu.is')) {
- webUrl = 'https://web.syu.is';
- }
-
- return {
- ...record,
- value: {
- ...record.value,
- author: {
- ...record.value.author,
- avatar: profile.avatar,
- displayName: profile.displayName || record.value.author.handle,
- _webUrl: webUrl, // Store network config for profile URL generation
- }
- }
- };
- }
- } catch (err) {
- // Ignore enhancement errors
- }
- }
- return record;
- })
- );
-
-
- // デバッグ情報を追加
-
- setComments(enhancedComments);
-
- // キャッシュに保存(5分間有効)
- if (pageUrl) {
- const cacheKey = 'cached_comments_' + new URL(pageUrl).pathname;
- const cacheData = {
- comments: enhancedComments,
- timestamp: Date.now()
- };
- localStorage.setItem(cacheKey, JSON.stringify(cacheData));
- }
- } catch (err) {
- setComments([]);
- }
- };
-
-
- const handlePostComment = async () => {
- if (!user || !commentText.trim()) {
- return;
- }
-
- setIsPosting(true);
- setError(null);
-
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) {
- throw new Error('No agent available');
- }
-
- // Create comment record with post-specific rkey
- const now = new Date();
- // Use post rkey if on post page, otherwise use timestamp-based rkey
- const rkey = appConfig.rkey || now.toISOString().replace(/[:.]/g, '-');
-
- const newComment = {
- text: commentText,
- url: window.location.href,
- createdAt: now.toISOString(),
- author: {
- did: user.did,
- handle: user.handle,
- avatar: user.avatar,
- displayName: user.displayName || user.handle,
- },
- };
-
- // Check if record with this rkey already exists
- let existingComments = [];
- try {
- const existingResponse = await agent.api.com.atproto.repo.getRecord({
- repo: user.did,
- collection: getCollectionNames(appConfig.collections.base).comment,
- rkey: rkey,
- });
-
- // Handle both old single comment format and new array format
- if (existingResponse.data.value.comments) {
- // New format: array of comments
- existingComments = existingResponse.data.value.comments;
- } else if (existingResponse.data.value.text) {
- // Old format: single comment, convert to array
- existingComments = [{
- text: existingResponse.data.value.text,
- url: existingResponse.data.value.url,
- createdAt: existingResponse.data.value.createdAt,
- author: existingResponse.data.value.author,
- }];
- }
- } catch (err) {
- // Record doesn't exist yet, that's fine
- }
-
- // Add new comment to the array
- existingComments.push(newComment);
-
- // Create the record with comments array
- const record = {
- $type: getCollectionNames(appConfig.collections.base).comment,
- comments: existingComments,
- url: window.location.href,
- createdAt: now.toISOString(), // Latest update time
- };
-
- // Post to ATProto with rkey
- const response = await agent.api.com.atproto.repo.putRecord({
- repo: user.did,
- collection: getCollectionNames(appConfig.collections.base).comment,
- rkey: rkey,
- record: record,
- });
-
-
- // Clear form and reload all comments
- setCommentText('');
- await loadAllComments(window.location.href);
- } catch (err: any) {
- setError('コメントの投稿に失敗しました: ' + err.message);
- } finally {
- setIsPosting(false);
- }
- };
-
- const handleDeleteComment = async (uri: string) => {
- if (!user) {
- alert('ログインが必要です');
- return;
- }
-
- if (!confirm('このコメントを削除しますか?')) {
- return;
- }
-
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) {
- throw new Error('No agent available');
- }
-
- // Extract rkey from URI: at://did:plc:xxx/ai.syui.log/rkey
- const uriParts = uri.split('/');
- const rkey = uriParts[uriParts.length - 1];
-
-
- // Delete the record
- await agent.api.com.atproto.repo.deleteRecord({
- repo: user.did,
- collection: getCollectionNames(appConfig.collections.base).comment,
- rkey: rkey,
- });
-
-
- // Reload all comments to reflect the deletion
- await loadAllComments(window.location.href);
-
- } catch (err: any) {
- alert('コメントの削除に失敗しました: ' + err.message);
- }
- };
-
- const handleLogout = async () => {
- // Logout from both services
- await authService.logout();
- atprotoOAuthService.logout();
- setUser(null);
- setComments([]);
- };
-
- // 管理者チェック
- const isAdmin = (user: User | null): boolean => {
- return user?.did === adminDid || user?.did === appConfig.adminDid;
- };
-
- // ユーザーリスト投稿
- const handlePostUserList = async () => {
- if (!user || !userListInput.trim()) {
- return;
- }
-
- if (!isAdmin(user)) {
- alert('管理者のみがユーザーリストを更新できます');
- return;
- }
-
- setIsPostingUserList(true);
- setError(null);
-
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) {
- throw new Error('No agent available');
- }
-
- // ユーザーリストをパース
- const userHandles = userListInput
- .split(',')
- .map(handle => handle.trim())
- .filter(handle => handle.length > 0);
-
- // ユーザーリストを各PDS用に分類し、実際のDIDを解決
- const users = await Promise.all(userHandles.map(async (handle) => {
- const pds = handle.endsWith('.syu.is') ? 'https://syu.is' : 'https://bsky.social';
-
- // 実際のDIDを解決
- let resolvedDid = `did:plc:${handle.replace(/\./g, '-')}-placeholder`; // フォールバック
-
- try {
- // Public APIでプロフィールを取得してDIDを解決
- const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
- if (profileResponse.ok) {
- const profileData = await profileResponse.json();
- if (profileData.did) {
- resolvedDid = profileData.did;
- }
- }
- } catch (err) {
- }
-
- return {
- handle: handle,
- pds: pds,
- did: resolvedDid
- };
- }));
-
- // Create user list record with ISO datetime rkey
- const now = new Date();
- const rkey = now.toISOString().replace(/[:.]/g, '-');
-
- const record = {
- $type: getCollectionNames(appConfig.collections.base).user,
- users: users,
- createdAt: now.toISOString(),
- updatedBy: {
- did: user.did,
- handle: user.handle,
- },
- };
-
- // Post to ATProto with rkey
- const response = await agent.api.com.atproto.repo.putRecord({
- repo: user.did,
- collection: getCollectionNames(appConfig.collections.base).user,
- rkey: rkey,
- record: record,
- });
-
-
- // Clear form and reload user list records
- setUserListInput('');
- loadUserListRecords();
- alert('ユーザーリストが更新されました');
- } catch (err: any) {
- setError('ユーザーリストの投稿に失敗しました: ' + err.message);
- } finally {
- setIsPostingUserList(false);
- }
- };
-
- // ユーザーリスト削除
- const handleDeleteUserList = async (uri: string) => {
- if (!user || !isAdmin(user)) {
- alert('管理者のみがユーザーリストを削除できます');
- return;
- }
-
- if (!confirm('このユーザーリストを削除しますか?')) {
- return;
- }
-
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) {
- throw new Error('No agent available');
- }
-
- // Extract rkey from URI
- const uriParts = uri.split('/');
- const rkey = uriParts[uriParts.length - 1];
-
-
- // Delete the record
- await agent.api.com.atproto.repo.deleteRecord({
- repo: user.did,
- collection: getCollectionNames(appConfig.collections.base).user,
- rkey: rkey,
- });
-
- loadUserListRecords();
- alert('ユーザーリストが削除されました');
-
- } catch (err: any) {
- alert('ユーザーリストの削除に失敗しました: ' + err.message);
- }
- };
-
- // JSON表示のトグル
- const toggleJsonDisplay = (uri: string) => {
- if (showJsonFor === uri) {
- setShowJsonFor(null);
- } else {
- setShowJsonFor(uri);
- }
- };
-
- // OAuth実行関数
- const executeOAuth = async () => {
- if (!handleInput.trim()) {
- alert('Please enter your Bluesky handle first');
- return;
- }
- try {
- await atprotoOAuthService.initiateOAuthFlow(handleInput);
- } catch (err) {
- alert('認証の開始に失敗しました。再度お試しください。');
- }
- };
-
- // ユーザーハンドルからプロフィールURLを生成
- const generateProfileUrl = (author: any): string => {
- // Check if this is admin/AI handle that should use configured PDS
- if (author.handle === appConfig.adminHandle || author.handle === appConfig.aiHandle) {
- const config = getNetworkConfig(appConfig.atprotoPds);
- return `${config.webUrl}/profile/${author.did}`;
- }
-
- // For ai.syu.is handle, also use configured PDS
- if (author.handle === 'ai.syu.is') {
- const config = getNetworkConfig(appConfig.atprotoPds);
- return `${config.webUrl}/profile/${author.did}`;
- }
-
- // For other users, detect network based on avatar URL or stored network info
- if (author.avatar && author.avatar.includes('cdn.bsky.app')) {
- // User has Bluesky avatar, use Bluesky web interface
- return `https://bsky.app/profile/${author.did}`;
- }
-
- // Check if we have stored network config from profile fetching
- if (author._webUrl) {
- return `${author._webUrl}/profile/${author.did}`;
- }
-
- // Fallback: Get PDS from handle for other users
- const pds = detectPdsFromHandle(author.handle);
- const config = getNetworkConfig(pds);
-
- // Use DID for profile URL
- return `${config.webUrl}/profile/${author.did}`;
- };
-
- // Rkey-based comment filtering
- // If on post page (/posts/xxx.html), only show comments with rkey=xxx
- const shouldShowComment = (record: any): boolean => {
- // If not on a post page, show all comments
- if (!appConfig.rkey) {
- return true;
- }
-
- // Extract rkey from comment URI: at://did:plc:xxx/collection/rkey
- // Handle both original records and flattened records from new array format
- const uri = record.uri || record.originalRecord?.uri;
- if (!uri) {
- return false;
- }
-
- const uriParts = uri.split('/');
- const commentRkey = uriParts[uriParts.length - 1];
-
- // Show comment only if rkey matches current post
- return commentRkey === appConfig.rkey;
- };
-
- // OAuth callback is now handled by React Router in main.tsx
-
- // Unified rendering function for AI content
- const renderAIContent = (record: any, index: number, className: string) => {
- // Handle both new format (record.value.$type) and old format compatibility
- const value = record.value;
- const isNewFormat = value.$type && value.post && value.author;
-
- // Extract content based on format
- const contentText = isNewFormat ? value.text : (value.content || value.body || '');
- // Use the author from the record if available, otherwise fall back to AI profile
- const authorInfo = value.author || aiProfile;
-
- const postInfo = isNewFormat ? value.post : null;
- const contentType = value.type || 'unknown';
- const createdAt = value.createdAt || value.generated_at || '';
-
- return (
-
-
-

-
-
- {new Date(createdAt).toLocaleString()}
-
-
-
-
-
-
-
-
- {/* JSON Display */}
- {showJsonFor === record.uri && (
-
-
JSON Record:
-
- {JSON.stringify(record, null, 2)}
-
-
- )}
-
-
- {contentText?.split('\n').map((line: string, index: number) => (
-
- {line}
- {index < contentText.split('\n').length - 1 &&
}
-
- ))}
-
-
- );
- };
-
- const getTypeLabel = (collectionType: string, contentType: string) => {
- if (!collectionType) return contentType;
-
- const collections = getCollectionNames(appConfig.collections.base);
-
- if (collectionType === collections.chat) {
- return contentType === 'question' ? '質問' : '回答';
- }
- if (collectionType === collections.chatLang) {
- return `翻訳: ${contentType.toUpperCase()}`;
- }
- if (collectionType === collections.chatComment) {
- return `AI ${contentType}`;
- }
- return contentType;
- };
-
- return (
-
-
-
-
- {/* Authentication Section */}
- {!user ? (
-
- setHandleInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- executeOAuth();
- }
- }}
- />
-
-
- ) : (
-
-
-
-

-
-
{user.displayName || user.handle}
-
@{user.handle}
-
{user.did}
-
-
-
-
-
- {/* Admin Section - User Management */}
- {isAdmin(user) && (
-
-
管理者機能 - ユーザーリスト管理
-
- {/* User List Form */}
-
-
- {/* User List Records */}
-
-
ユーザーリスト一覧 ({userListRecords.length}件)
- {userListRecords.length === 0 ? (
-
ユーザーリストが見つかりません
- ) : (
- userListRecords.map((record, index) => (
-
-
-
- {new Date(record.value.createdAt).toLocaleString()}
-
-
-
-
-
-
-
-
- {record.value.users && record.value.users.map((user, userIndex) => (
-
- {user.handle}
- ({new URL(user.pds).hostname})
-
- ))}
-
-
- URI: {record.uri}
-
- Updated by: {record.value.updatedBy?.handle || 'unknown'}
-
-
- {/* JSON Display */}
- {showJsonFor === record.uri && (
-
-
JSON Record:
-
- {JSON.stringify(record, null, 2)}
-
-
- )}
-
-
- ))
- )}
-
-
- )}
-
-
- )}
-
- {/* Tab Navigation */}
-
-
-
-
-
-
-
- {/* Comments List */}
- {activeTab === 'comments' && (
-
- {comments.filter(shouldShowComment).length === 0 ? (
-
- {appConfig.rkey ? `No comments for this post yet` : `No comments yet`}
-
- ) : (
- comments.filter(shouldShowComment).map((record, index) => (
-
-
-

-
-
- {new Date(record.value.createdAt).toLocaleString()}
-
-
-
- {/* Show delete button only for current user's comments */}
- {user && record.value.author?.did === user.did && (
-
- )}
-
-
-
-
-
- {/* JSON Display */}
- {showJsonFor === record.uri && (
-
-
JSON Record:
-
- {JSON.stringify(record, null, 2)}
-
-
- )}
-
-
- {record.value.text?.split('\n').map((line: string, index: number) => (
-
- {line}
- {index < record.value.text.split('\n').length - 1 &&
}
-
- ))}
-
-
- ))
- )}
-
- )}
-
- {/* AI Chat History List */}
- {activeTab === 'ai-chat' && (
-
- {aiChatHistory.length === 0 ? (
-
No AI conversations yet. Start chatting with Ask AI!
- ) : (
- aiChatHistory.map((record, index) =>
- renderAIContent(record, index, 'comment-item')
- )
- )}
-
- )}
-
- {/* Lang: EN List */}
- {activeTab === 'lang-en' && (
-
- {langEnRecords.length === 0 ? (
-
No EN translations yet
- ) : (
- langEnRecords.map((record, index) =>
- renderAIContent(record, index, 'lang-item')
- )
- )}
-
- )}
-
- {/* AI Comment List */}
- {activeTab === 'ai-comment' && (
-
- {aiCommentRecords.length === 0 ? (
-
No AI comments yet
- ) : (
- aiCommentRecords.map((record, index) =>
- renderAIContent(record, index, 'comment-item')
- )
- )}
-
- )}
-
- {/* Comment Form - Only show on post pages when Comments tab is active */}
- {user && appConfig.rkey && activeTab === 'comments' && (
-
-
- )}
-
-
-
-
- {/* AI Chat Component - handles all AI functionality */}
-
-
- );
-}
-
-export default App;
diff --git a/oauth_new/src/api/atproto.js b/oauth/src/api/atproto.js
similarity index 100%
rename from oauth_new/src/api/atproto.js
rename to oauth/src/api/atproto.js
diff --git a/oauth/src/components/AIChat-access.tsx b/oauth/src/components/AIChat-access.tsx
deleted file mode 100644
index f808354..0000000
--- a/oauth/src/components/AIChat-access.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-// Cloudflare Access対応版の例
-const response = await fetch(`${aiConfig.host}/api/generate`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- // Cloudflare Access Service Token
- 'CF-Access-Client-Id': import.meta.env.VITE_CF_ACCESS_CLIENT_ID,
- 'CF-Access-Client-Secret': import.meta.env.VITE_CF_ACCESS_CLIENT_SECRET,
- },
- body: JSON.stringify({
- model: aiConfig.model,
- prompt: prompt,
- stream: false,
- options: {
- temperature: 0.9,
- top_p: 0.9,
- num_predict: 200,
- repeat_penalty: 1.1,
- }
- }),
-});
\ No newline at end of file
diff --git a/oauth/src/components/AIChat.tsx b/oauth/src/components/AIChat.tsx
deleted file mode 100644
index 72b8bf3..0000000
--- a/oauth/src/components/AIChat.tsx
+++ /dev/null
@@ -1,271 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { User } from '../services/auth';
-import { atprotoOAuthService } from '../services/atproto-oauth';
-import { appConfig, getCollectionNames } from '../config/app';
-
-interface AIChatProps {
- user: User | null;
- isEnabled: boolean;
-}
-
-export const AIChat: React.FC = ({ user, isEnabled }) => {
- const [chatHistory, setChatHistory] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [isProcessing, setIsProcessing] = useState(false);
- const [aiProfile, setAiProfile] = useState(null);
-
- // Get AI settings from appConfig (unified configuration)
- const aiConfig = {
- enabled: appConfig.aiEnabled,
- askAi: appConfig.aiAskAi,
- provider: appConfig.aiProvider,
- model: appConfig.aiModel,
- host: appConfig.aiHost,
- systemPrompt: appConfig.aiSystemPrompt,
- aiDid: appConfig.aiDid,
- bskyPublicApi: appConfig.bskyPublicApi,
- };
-
- // Fetch AI profile on load
- useEffect(() => {
- const fetchAIProfile = async () => {
- if (!aiConfig.aiDid) {
- return;
- }
-
- try {
- // Try with agent first
- const agent = atprotoOAuthService.getAgent();
- if (agent) {
- const profile = await agent.getProfile({ actor: aiConfig.aiDid });
- const profileData = {
- did: aiConfig.aiDid,
- handle: profile.data.handle,
- displayName: profile.data.displayName,
- avatar: profile.data.avatar,
- description: profile.data.description
- };
- setAiProfile(profileData);
-
- // Dispatch event to update Ask AI button
- window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profileData }));
- return;
- }
-
- // Fallback to public API
- const response = await fetch(`${aiConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(aiConfig.aiDid)}`);
- if (response.ok) {
- const profileData = await response.json();
- const profile = {
- did: aiConfig.aiDid,
- handle: profileData.handle,
- displayName: profileData.displayName,
- avatar: profileData.avatar,
- description: profileData.description
- };
- setAiProfile(profile);
-
- // Dispatch event to update Ask AI button
- window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profile }));
- return;
- }
- } catch (error) {
- setAiProfile(null);
- }
- };
-
- fetchAIProfile();
- }, [aiConfig.aiDid]);
-
- useEffect(() => {
- if (!isEnabled || !aiConfig.askAi) return;
-
- // Listen for AI question posts from base.html
- const handleAIQuestion = async (event: any) => {
- if (!user || !event.detail || !event.detail.question || isProcessing || !aiProfile) return;
-
- setIsProcessing(true);
- try {
- await postQuestionAndGenerateResponse(event.detail.question);
- } finally {
- setIsProcessing(false);
- }
- };
-
- // Add listener with a small delay to ensure it's ready
- setTimeout(() => {
- window.addEventListener('postAIQuestion', handleAIQuestion);
-
- // Notify that AI is ready
- window.dispatchEvent(new CustomEvent('aiChatReady'));
- }, 100);
-
- return () => {
- window.removeEventListener('postAIQuestion', handleAIQuestion);
- };
- }, [user, isEnabled, isProcessing, aiProfile]);
-
- const postQuestionAndGenerateResponse = async (question: string) => {
- if (!user || !aiConfig.askAi || !aiProfile) return;
-
- setIsLoading(true);
-
- try {
- const agent = atprotoOAuthService.getAgent();
- if (!agent) throw new Error('No agent available');
-
- // Get collection names
- const collections = getCollectionNames(appConfig.collections.base);
-
- // 1. Post question to ATProto
- const now = new Date();
- const rkey = now.toISOString().replace(/[:.]/g, '-');
-
- // Extract post metadata from current page
- const currentUrl = window.location.href;
- const postSlug = currentUrl.match(/\/posts\/([^/]+)/)?.[1] || '';
- const postTitle = document.title.replace(' - syui.ai', '') || '';
-
- const questionRecord = {
- $type: collections.chat,
- post: {
- url: currentUrl,
- slug: postSlug,
- title: postTitle,
- date: new Date().toISOString(),
- tags: [],
- language: "ja"
- },
- type: "question",
- text: question,
- author: {
- did: user.did,
- handle: user.handle,
- avatar: user.avatar,
- displayName: user.displayName || user.handle,
- },
- createdAt: now.toISOString(),
- };
-
- await agent.api.com.atproto.repo.putRecord({
- repo: user.did,
- collection: collections.chat,
- rkey: rkey,
- record: questionRecord,
- });
-
- // 2. Get chat history
- const chatRecords = await agent.api.com.atproto.repo.listRecords({
- repo: user.did,
- collection: collections.chat,
- limit: 10,
- });
-
- let chatHistoryText = '';
- if (chatRecords.data.records) {
- chatHistoryText = chatRecords.data.records
- .map((r: any) => {
- if (r.value.type === 'question') {
- return `User: ${r.value.text}`;
- } else if (r.value.type === 'answer') {
- return `AI: ${r.value.text}`;
- }
- return '';
- })
- .filter(Boolean)
- .join('\n');
- }
-
- // 3. Generate AI response based on provider
- let aiAnswer = '';
-
- // 3. Generate AI response using Ollama via proxy
- if (aiConfig.provider === 'ollama') {
- const prompt = `${aiConfig.systemPrompt}
-
-Question: ${question}
-
-Answer:`;
-
- const response = await fetch(`${aiConfig.host}/api/generate`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Origin': 'https://syui.ai',
- },
- body: JSON.stringify({
- model: aiConfig.model,
- prompt: prompt,
- stream: false,
- options: {
- temperature: 0.9,
- top_p: 0.9,
- num_predict: 200, // Longer responses for better answers
- repeat_penalty: 1.1,
- }
- }),
- });
-
- if (!response.ok) {
- throw new Error('AI API request failed');
- }
-
- const data = await response.json();
- aiAnswer = data.response;
- }
-
- // 4. Immediately dispatch event to update UI
- window.dispatchEvent(new CustomEvent('aiResponseReceived', {
- detail: {
- answer: aiAnswer,
- aiProfile: aiProfile,
- timestamp: now.toISOString()
- }
- }));
-
- // 5. Save AI response in background
- const answerRkey = now.toISOString().replace(/[:.]/g, '-') + '-answer';
-
- const answerRecord = {
- $type: collections.chat,
- post: {
- url: currentUrl,
- slug: postSlug,
- title: postTitle,
- date: new Date().toISOString(),
- tags: [],
- language: "ja"
- },
- type: "answer",
- text: aiAnswer,
- author: {
- did: aiProfile.did,
- handle: aiProfile.handle,
- displayName: aiProfile.displayName,
- avatar: aiProfile.avatar,
- },
- createdAt: now.toISOString(),
- };
-
- // Save to ATProto asynchronously (don't wait for it)
- agent.api.com.atproto.repo.putRecord({
- repo: user.did,
- collection: collections.chat,
- rkey: answerRkey,
- record: answerRecord,
- }).catch(err => {
- // Silent fail for AI response saving
- });
-
- } catch (error) {
- window.dispatchEvent(new CustomEvent('aiResponseError', {
- detail: { error: 'AI応答の生成に失敗しました' }
- }));
- } finally {
- setIsLoading(false);
- }
- };
-
- // This component doesn't render anything - it just handles the logic
- return null;
-};
\ No newline at end of file
diff --git a/oauth/src/components/AIProfile.tsx b/oauth/src/components/AIProfile.tsx
deleted file mode 100644
index 4ca05f2..0000000
--- a/oauth/src/components/AIProfile.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { AtprotoAgent } from '@atproto/api';
-
-interface AIProfile {
- did: string;
- handle: string;
- displayName?: string;
- avatar?: string;
- description?: string;
-}
-
-interface AIProfileProps {
- aiDid: string;
-}
-
-export const AIProfile: React.FC = ({ aiDid }) => {
- const [profile, setProfile] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const fetchAIProfile = async () => {
- try {
- // Use public API to get profile information
- const agent = new AtprotoAgent({ service: 'https://bsky.social' });
- const response = await agent.getProfile({ actor: aiDid });
-
- setProfile({
- did: response.data.did,
- handle: response.data.handle,
- displayName: response.data.displayName,
- avatar: response.data.avatar,
- description: response.data.description,
- });
- } catch (error) {
- // Failed to fetch AI profile
- // Fallback to basic info
- setProfile({
- did: aiDid,
- handle: 'ai-assistant',
- displayName: 'AI Assistant',
- description: 'AI assistant for this blog',
- });
- } finally {
- setLoading(false);
- }
- };
-
- if (aiDid) {
- fetchAIProfile();
- }
- }, [aiDid]);
-
- if (loading) {
- return Loading AI profile...
;
- }
-
- if (!profile) {
- return null;
- }
-
- return (
-
-
- {profile.avatar ? (
-

- ) : (
-
🤖
- )}
-
-
-
{profile.displayName || profile.handle}
-
@{profile.handle}
- {profile.description && (
-
{profile.description}
- )}
-
-
- );
-};
\ No newline at end of file
diff --git a/oauth_new/src/components/AskAI.jsx b/oauth/src/components/AskAI.jsx
similarity index 100%
rename from oauth_new/src/components/AskAI.jsx
rename to oauth/src/components/AskAI.jsx
diff --git a/oauth_new/src/components/AuthButton.jsx b/oauth/src/components/AuthButton.jsx
similarity index 100%
rename from oauth_new/src/components/AuthButton.jsx
rename to oauth/src/components/AuthButton.jsx
diff --git a/oauth_new/src/components/Avatar.jsx b/oauth/src/components/Avatar.jsx
similarity index 100%
rename from oauth_new/src/components/Avatar.jsx
rename to oauth/src/components/Avatar.jsx
diff --git a/oauth_new/src/components/AvatarImage.jsx b/oauth/src/components/AvatarImage.jsx
similarity index 100%
rename from oauth_new/src/components/AvatarImage.jsx
rename to oauth/src/components/AvatarImage.jsx
diff --git a/oauth_new/src/components/AvatarTest.jsx b/oauth/src/components/AvatarTest.jsx
similarity index 100%
rename from oauth_new/src/components/AvatarTest.jsx
rename to oauth/src/components/AvatarTest.jsx
diff --git a/oauth_new/src/components/AvatarTestPanel.jsx b/oauth/src/components/AvatarTestPanel.jsx
similarity index 100%
rename from oauth_new/src/components/AvatarTestPanel.jsx
rename to oauth/src/components/AvatarTestPanel.jsx
diff --git a/oauth/src/components/Card.tsx b/oauth/src/components/Card.tsx
deleted file mode 100644
index 4d6dc2e..0000000
--- a/oauth/src/components/Card.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import { Card as CardType, CardRarity } from '../types/card';
-import '../styles/Card.css';
-
-interface CardProps {
- card: CardType;
- isRevealing?: boolean;
- detailed?: boolean;
-}
-
-const CARD_INFO: Record = {
- 0: { name: "アイ", color: "#fff700" },
- 1: { name: "夢幻", color: "#b19cd9" },
- 2: { name: "光彩", color: "#ffd700" },
- 3: { name: "中性子", color: "#cacfd2" },
- 4: { name: "太陽", color: "#ff6b35" },
- 5: { name: "夜空", color: "#1a1a2e" },
- 6: { name: "雪", color: "#e3f2fd" },
- 7: { name: "雷", color: "#ffd93d" },
- 8: { name: "超究", color: "#6c5ce7" },
- 9: { name: "剣", color: "#a8e6cf" },
- 10: { name: "破壊", color: "#ff4757" },
- 11: { name: "地球", color: "#4834d4" },
- 12: { name: "天の川", color: "#9c88ff" },
- 13: { name: "創造", color: "#00d2d3" },
- 14: { name: "超新星", color: "#ff9ff3" },
- 15: { name: "世界", color: "#54a0ff" },
-};
-
-export const Card: React.FC = ({ card, isRevealing = false, detailed = false }) => {
- const cardInfo = CARD_INFO[card.id] || { name: "Unknown", color: "#666" };
- const imageUrl = `https://git.syui.ai/ai/card/raw/branch/main/img/${card.id}.webp`;
-
- const getRarityClass = () => {
- switch (card.status) {
- case CardRarity.UNIQUE:
- return 'card-unique';
- case CardRarity.KIRA:
- return 'card-kira';
- case CardRarity.SUPER_RARE:
- return 'card-super-rare';
- case CardRarity.RARE:
- return 'card-rare';
- default:
- return 'card-normal';
- }
- };
-
- if (!detailed) {
- // Simple view - only image and frame
- return (
-
-
-

{
- (e.target as HTMLImageElement).style.display = 'none';
- }}
- />
-
-
- );
- }
-
- // Detailed view - all information
- return (
-
-
-
- #{card.id}
- CP: {card.cp}
-
-
-
-

{
- (e.target as HTMLImageElement).style.display = 'none';
- }}
- />
-
-
-
-
{cardInfo.name}
- {card.is_unique && (
-
UNIQUE
- )}
-
-
- {card.skill && (
-
- )}
-
-
- {card.status.toUpperCase()}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/oauth/src/components/CardBox.tsx b/oauth/src/components/CardBox.tsx
deleted file mode 100644
index 673e047..0000000
--- a/oauth/src/components/CardBox.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { atprotoOAuthService } from '../services/atproto-oauth';
-import { Card } from './Card';
-import '../styles/CardBox.css';
-
-interface CardBoxProps {
- userDid: string;
-}
-
-export const CardBox: React.FC = ({ userDid }) => {
- const [boxData, setBoxData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [showJson, setShowJson] = useState(false);
- const [isDeleting, setIsDeleting] = useState(false);
-
- useEffect(() => {
- loadBoxData();
- }, [userDid]);
-
- const loadBoxData = async () => {
- setLoading(true);
- setError(null);
-
- try {
- const data = await atprotoOAuthService.getCardsFromBox();
- setBoxData(data);
- } catch (err) {
- // Failed to load card box
- setError(err instanceof Error ? err.message : 'カードボックスの読み込みに失敗しました');
- } finally {
- setLoading(false);
- }
- };
-
- const handleSaveToBox = async () => {
- // 現在のカードデータを取得してボックスに保存
- // この部分は親コンポーネントから渡すか、APIから取得する必要があります
- alert('カードボックスへの保存機能は親コンポーネントから実行してください');
- };
-
- const handleDeleteBox = async () => {
- if (!window.confirm('カードボックスを削除してもよろしいですか?\nこの操作は取り消せません。')) {
- return;
- }
-
- setIsDeleting(true);
- setError(null);
-
- try {
- await atprotoOAuthService.deleteCardBox();
- setBoxData({ records: [] });
- alert('カードボックスを削除しました');
- } catch (err) {
- // Failed to delete card box
- setError(err instanceof Error ? err.message : 'カードボックスの削除に失敗しました');
- } finally {
- setIsDeleting(false);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (error) {
- return (
-
-
エラー: {error}
-
-
- );
- }
-
- const records = boxData?.records || [];
- const selfRecord = records.find((record: any) => record.uri.includes('/self'));
- const cards = selfRecord?.value?.cards || [];
-
- return (
-
-
-
📦 atproto カードボックス
-
-
-
- {cards.length > 0 && (
-
- )}
-
-
-
-
-
- 📍 URI:
- at://did:plc:uqzpqmrjnptsxezjx4xuh2mn/ai.card.box/self
-
-
-
- {showJson && (
-
-
Raw JSON データ:
-
- {JSON.stringify(boxData, null, 2)}
-
-
- )}
-
-
-
- 総カード数: {cards.length}枚
- {selfRecord?.value?.updated_at && (
- <>
-
- 最終更新: {new Date(selfRecord.value.updated_at).toLocaleString()}
- >
- )}
-
-
-
- {cards.length > 0 ? (
- <>
-
- {cards.map((card: any, index: number) => (
-
-
-
- ID: {card.id} | CP: {card.cp}
-
-
- ))}
-
- >
- ) : (
-
-
カードボックスにカードがありません
-
カードを引いてからバックアップボタンを押してください
-
- )}
-
- );
-};
\ No newline at end of file
diff --git a/oauth/src/components/CardList.tsx b/oauth/src/components/CardList.tsx
deleted file mode 100644
index f708cb8..0000000
--- a/oauth/src/components/CardList.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Card } from './Card';
-import { cardApi } from '../services/api';
-import { Card as CardType } from '../types/card';
-import '../styles/CardList.css';
-
-interface CardMasterData {
- id: number;
- name: string;
- ja_name: string;
- description: string;
- base_cp_min: number;
- base_cp_max: number;
-}
-
-export const CardList: React.FC = () => {
- const [loading, setLoading] = useState(true);
- const [masterData, setMasterData] = useState([]);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- loadMasterData();
- }, []);
-
- const loadMasterData = async () => {
- try {
- setLoading(true);
- const response = await fetch('http://localhost:8000/api/v1/cards/master');
- if (!response.ok) {
- throw new Error('Failed to fetch card master data');
- }
- const data = await response.json();
- setMasterData(data);
- } catch (err) {
- // Failed to load card master data
- setError(err instanceof Error ? err.message : 'Failed to load card data');
- } finally {
- setLoading(false);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- if (error) {
- return (
-
-
Error: {error}
-
-
- );
- }
-
- // Create cards for all rarity patterns
- const rarityPatterns = ['normal', 'unique'] as const;
-
- const displayCards: Array<{card: CardType, data: CardMasterData, patternName: string}> = [];
-
- masterData.forEach(data => {
- rarityPatterns.forEach(pattern => {
- const card: CardType = {
- id: data.id,
- cp: Math.floor((data.base_cp_min + data.base_cp_max) / 2),
- status: pattern,
- skill: null,
- owner_did: 'sample',
- obtained_at: new Date().toISOString(),
- is_unique: pattern === 'unique',
- unique_id: pattern === 'unique' ? 'sample-unique-id' : null
- };
- displayCards.push({
- card,
- data,
- patternName: `${data.id}-${pattern}`
- });
- });
- });
-
-
- return (
-
-
-
-
- {displayCards.map(({ card, data, patternName }) => (
-
-
-
-
ID: {data.id}
-
Name: {data.name}
-
日本語名: {data.ja_name}
-
レアリティ: {card.status}
-
CP: {card.cp}
-
CP範囲: {data.base_cp_min}-{data.base_cp_max}
- {data.description && (
-
{data.description}
- )}
-
-
- ))}
-
-
- );
-};
\ No newline at end of file
diff --git a/oauth/src/components/CollectionAnalysis.tsx b/oauth/src/components/CollectionAnalysis.tsx
deleted file mode 100644
index 327d9b7..0000000
--- a/oauth/src/components/CollectionAnalysis.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { aiCardApi } from '../services/api';
-import '../styles/CollectionAnalysis.css';
-
-interface AnalysisData {
- total_cards: number;
- unique_cards: number;
- rarity_distribution: Record;
- collection_score: number;
- recommendations: string[];
-}
-
-interface CollectionAnalysisProps {
- userDid: string;
-}
-
-export const CollectionAnalysis: React.FC = ({ userDid }) => {
- const [analysis, setAnalysis] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const loadAnalysis = async () => {
- if (!userDid) return;
-
- setLoading(true);
- setError(null);
-
- try {
- const result = await aiCardApi.analyzeCollection(userDid);
- setAnalysis(result);
- } catch (err) {
- // Collection analysis failed
- setError('AI分析機能を利用するにはai.gptサーバーが必要です。基本機能はai.cardサーバーのみで利用できます。');
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- loadAnalysis();
- }, [userDid]);
-
- if (loading) {
- return (
-
- );
- }
-
- if (error) {
- return (
-
- );
- }
-
- if (!analysis) {
- return (
-
-
-
分析データがありません
-
-
-
- );
- }
-
- return (
-
-
🧠 AI コレクション分析
-
-
-
-
{analysis.total_cards}
-
総カード数
-
-
-
{analysis.unique_cards}
-
ユニークカード
-
-
-
{analysis.collection_score}
-
コレクションスコア
-
-
-
-
-
レアリティ分布
-
- {Object.entries(analysis.rarity_distribution).map(([rarity, count]) => (
-
- ))}
-
-
-
- {analysis.recommendations && analysis.recommendations.length > 0 && (
-
-
🎯 AI推奨
-
- {analysis.recommendations.map((rec, index) => (
- - {rec}
- ))}
-
-
- )}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/oauth_new/src/components/CommentForm.jsx b/oauth/src/components/CommentForm.jsx
similarity index 80%
rename from oauth_new/src/components/CommentForm.jsx
rename to oauth/src/components/CommentForm.jsx
index 1f7c615..83c2743 100644
--- a/oauth_new/src/components/CommentForm.jsx
+++ b/oauth/src/components/CommentForm.jsx
@@ -2,21 +2,34 @@ import React, { useState } from 'react'
import { atproto, collections } from '../api/atproto.js'
import { env } from '../config/env.js'
-export default function CommentForm({ user, agent, onCommentPosted }) {
+export default function CommentForm({ user, agent, onCommentPosted, pageContext }) {
const [text, setText] = useState('')
- const [url, setUrl] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
+
+ // Get current URL automatically, but exclude OAuth callback URLs
+ const getCurrentUrl = () => {
+ const currentPath = window.location.href
+
+ // If on OAuth callback page, get the stored return URL or use root
+ if (currentPath.includes('/oauth/callback')) {
+ return sessionStorage.getItem('oauth_return_url') || window.location.origin
+ }
+
+ // Remove hash fragments for clean URLs
+ return currentPath.split('#')[0]
+ }
+
+ const currentUrl = getCurrentUrl()
const handleSubmit = async (e) => {
e.preventDefault()
- if (!text.trim() || !url.trim()) return
+ if (!text.trim()) return
setLoading(true)
setError(null)
try {
- const currentUrl = url.trim()
const timestamp = new Date().toISOString()
// Create ai.syui.log record structure (new unified format)
@@ -55,7 +68,6 @@ export default function CommentForm({ user, agent, onCommentPosted }) {
// Clear form
setText('')
- setUrl('')
// Notify parent component
if (onCommentPosted) {
@@ -86,18 +98,8 @@ export default function CommentForm({ user, agent, onCommentPosted }) {
コメントを投稿