From 011077359277549841318828987cc2a0ee971646 Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 15 Jul 2025 17:32:20 +0900 Subject: [PATCH] test ai-blog --- .github/workflows/cloudflare-pages.yml | 16 + .gitignore | 1 + Cargo.toml | 4 +- ai-conversation/.env | 10 + ai-conversation/.env.production | 19 + ai-conversation/build-minimal.js | 29 + ai-conversation/index.html | 11 + .../json/ai.syui.ai_chat_comment.json | 62 + .../json/ai.syui.ai_chat_lang.json | 62 + ai-conversation/json/ai.syui.ai_log.json | 30 + ai-conversation/json/ai.syui.ai_user.json | 53 + ai-conversation/json/syui.syui.ai_chat.json | 53 + ai-conversation/json/syui.syui.ai_log.json | 3 + ai-conversation/package.json | 22 + ai-conversation/src/App.css | 1383 +++++++++++++++++ ai-conversation/src/App.jsx | 110 ++ ai-conversation/src/api/atproto.js | 192 +++ ai-conversation/src/components/AskAI.jsx | 399 +++++ ai-conversation/src/components/AuthButton.jsx | 77 + ai-conversation/src/components/Avatar.jsx | 234 +++ .../src/components/AvatarImage.jsx | 103 ++ ai-conversation/src/components/AvatarTest.jsx | 203 +++ .../src/components/AvatarTestPanel.jsx | 246 +++ .../src/components/ChatRecordList.jsx | 194 +++ .../src/components/CommentForm.jsx | 134 ++ .../src/components/LoadingSkeleton.jsx | 98 ++ .../src/components/OAuthCallback.jsx | 36 + .../src/components/ProfileForm.jsx | 165 ++ .../src/components/ProfileRecordList.jsx | 136 ++ ai-conversation/src/components/RecordList.jsx | 154 ++ ai-conversation/src/components/RecordTabs.jsx | 214 +++ ai-conversation/src/components/TestUI.jsx | 531 +++++++ ai-conversation/src/components/UserLookup.jsx | 115 ++ ai-conversation/src/config/env.js | 17 + ai-conversation/src/hooks/useAdminData.js | 135 ++ ai-conversation/src/hooks/useAskAI.js | 230 +++ ai-conversation/src/hooks/useAuth.js | 80 + ai-conversation/src/hooks/usePageContext.js | 33 + ai-conversation/src/hooks/useUserData.js | 215 +++ ai-conversation/src/main.jsx | 10 + ai-conversation/src/services/aiProvider.js | 214 +++ ai-conversation/src/services/oauth.js | 182 +++ ai-conversation/src/utils/avatar.js | 206 +++ ai-conversation/src/utils/avatarCache.js | 262 ++++ ai-conversation/src/utils/avatarFetcher.js | 158 ++ ai-conversation/src/utils/cache.js | 63 + ai-conversation/src/utils/errorHandler.js | 49 + ai-conversation/src/utils/logger.js | 82 + ai-conversation/src/utils/pds.js | 39 + ai-conversation/vite.config.js | 15 + my-blog/config.toml | 66 + my-blog/content/posts/2025-07-16-ccd52de8.md | 334 ++++ my-blog/layouts/shortcodes/msg.html | 14 + my-blog/static/css/style.css | 390 +++++ my-blog/static/oauth/index.html | 4 +- my-blog/templates/ai-conversation-assets.html | 3 + my-blog/templates/base.html | 18 + my-blog/templates/post.html | 20 +- oauth/src/App.css | 6 - oauth/src/App.jsx | 4 +- oauth/src/components/RecordTabs.jsx | 52 +- oauth/src/hooks/useAdminData.js | 46 +- oauth/src/hooks/useUserData.js | 43 +- oauth/src/main.jsx | 6 +- scpt/delete-chat-records.zsh | 4 +- scpt/run.zsh | 14 + src/atproto/mod.rs | 3 +- src/atproto/profile.rs | 213 +++ src/commands/auth.rs | 2 +- src/commands/interactive.rs | 680 ++++++++ src/commands/mod.rs | 3 +- src/generator.rs | 5 +- src/main.rs | 11 +- 73 files changed, 9000 insertions(+), 30 deletions(-) create mode 100644 ai-conversation/.env create mode 100644 ai-conversation/.env.production create mode 100644 ai-conversation/build-minimal.js create mode 100644 ai-conversation/index.html create mode 100644 ai-conversation/json/ai.syui.ai_chat_comment.json create mode 100644 ai-conversation/json/ai.syui.ai_chat_lang.json create mode 100644 ai-conversation/json/ai.syui.ai_log.json create mode 100644 ai-conversation/json/ai.syui.ai_user.json create mode 100644 ai-conversation/json/syui.syui.ai_chat.json create mode 100644 ai-conversation/json/syui.syui.ai_log.json create mode 100644 ai-conversation/package.json create mode 100644 ai-conversation/src/App.css create mode 100644 ai-conversation/src/App.jsx create mode 100644 ai-conversation/src/api/atproto.js create mode 100644 ai-conversation/src/components/AskAI.jsx create mode 100644 ai-conversation/src/components/AuthButton.jsx create mode 100644 ai-conversation/src/components/Avatar.jsx create mode 100644 ai-conversation/src/components/AvatarImage.jsx create mode 100644 ai-conversation/src/components/AvatarTest.jsx create mode 100644 ai-conversation/src/components/AvatarTestPanel.jsx create mode 100644 ai-conversation/src/components/ChatRecordList.jsx create mode 100644 ai-conversation/src/components/CommentForm.jsx create mode 100644 ai-conversation/src/components/LoadingSkeleton.jsx create mode 100644 ai-conversation/src/components/OAuthCallback.jsx create mode 100644 ai-conversation/src/components/ProfileForm.jsx create mode 100644 ai-conversation/src/components/ProfileRecordList.jsx create mode 100644 ai-conversation/src/components/RecordList.jsx create mode 100644 ai-conversation/src/components/RecordTabs.jsx create mode 100644 ai-conversation/src/components/TestUI.jsx create mode 100644 ai-conversation/src/components/UserLookup.jsx create mode 100644 ai-conversation/src/config/env.js create mode 100644 ai-conversation/src/hooks/useAdminData.js create mode 100644 ai-conversation/src/hooks/useAskAI.js create mode 100644 ai-conversation/src/hooks/useAuth.js create mode 100644 ai-conversation/src/hooks/usePageContext.js create mode 100644 ai-conversation/src/hooks/useUserData.js create mode 100644 ai-conversation/src/main.jsx create mode 100644 ai-conversation/src/services/aiProvider.js create mode 100644 ai-conversation/src/services/oauth.js create mode 100644 ai-conversation/src/utils/avatar.js create mode 100644 ai-conversation/src/utils/avatarCache.js create mode 100644 ai-conversation/src/utils/avatarFetcher.js create mode 100644 ai-conversation/src/utils/cache.js create mode 100644 ai-conversation/src/utils/errorHandler.js create mode 100644 ai-conversation/src/utils/logger.js create mode 100644 ai-conversation/src/utils/pds.js create mode 100644 ai-conversation/vite.config.js create mode 100644 my-blog/content/posts/2025-07-16-ccd52de8.md create mode 100644 my-blog/layouts/shortcodes/msg.html create mode 100644 my-blog/templates/ai-conversation-assets.html create mode 100644 src/atproto/profile.rs create mode 100644 src/commands/interactive.rs diff --git a/.github/workflows/cloudflare-pages.yml b/.github/workflows/cloudflare-pages.yml index fe82337..995e8d2 100644 --- a/.github/workflows/cloudflare-pages.yml +++ b/.github/workflows/cloudflare-pages.yml @@ -40,6 +40,22 @@ jobs: 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: Install AI conversation dependencies + run: | + cd ai-conversation + npm install + + - name: Build AI conversation app + run: | + cd ai-conversation + NODE_ENV=production npm run build + - name: Copy AI conversation build to static + run: | + rm -rf my-blog/static/ai-assets + mkdir -p my-blog/static/ai-assets + cp -rf ai-conversation/dist/assets/* my-blog/static/ai-assets/ + cp ai-conversation/dist/index.html my-blog/templates/ai-conversation-assets.html - name: Cache ailog binary uses: actions/cache@v4 diff --git a/.gitignore b/.gitignore index 1bec84e..0b4d966 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ dist node_modules package-lock.json my-blog/static/assets/comment-atproto-* +my-blog/static/ai-assets/comment-atproto-* bin/ailog docs my-blog/static/index.html diff --git a/Cargo.toml b/Cargo.toml index 4e398c5..004d89d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ailog" -version = "0.2.7" +version = "0.2.8" edition = "2021" authors = ["syui"] description = "A static blog generator with AI features" @@ -56,6 +56,8 @@ tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots", " futures-util = "0.3" tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"], default-features = false } rpassword = "7.3" +rustyline = "14.0" +dirs = "5.0" [dev-dependencies] tempfile = "3.14" diff --git a/ai-conversation/.env b/ai-conversation/.env new file mode 100644 index 0000000..801b270 --- /dev/null +++ b/ai-conversation/.env @@ -0,0 +1,10 @@ +VITE_ADMIN=ai.syui.ai +VITE_PDS=syu.is +VITE_HANDLE_LIST=["ai.syui.ai", "syui.syui.ai", "ai.ai"] +VITE_COLLECTION=ai.syui.log +VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json +VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback + +# Development/Debug features +VITE_ENABLE_TEST_UI=true +VITE_ENABLE_DEBUG=true diff --git a/ai-conversation/.env.production b/ai-conversation/.env.production new file mode 100644 index 0000000..1a90551 --- /dev/null +++ b/ai-conversation/.env.production @@ -0,0 +1,19 @@ +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 + +# AI Configuration - match oauth_old settings +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_ASK_AI_URL=https://ollama.syui.ai/api/generate +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/ai-conversation/build-minimal.js b/ai-conversation/build-minimal.js new file mode 100644 index 0000000..17f32e2 --- /dev/null +++ b/ai-conversation/build-minimal.js @@ -0,0 +1,29 @@ +// Create minimal index.html like oauth/dist/index.html format +import fs from 'fs' +import path from 'path' + +const distDir = './dist' +const indexPath = path.join(distDir, 'index.html') + +// Read the built index.html +const content = fs.readFileSync(indexPath, 'utf8') + +// Extract script and link tags +const scriptMatch = content.match(/]*src="([^"]*)"[^>]*><\/script>/) +const linkMatch = content.match(/]*href="([^"]*)"[^>]*>/) + +if (scriptMatch && linkMatch) { + // Replace /assets/ with /ai-assets/ for AI conversation assets + const scriptSrc = scriptMatch[1].replace('/assets/', '/ai-assets/') + const linkHref = linkMatch[1].replace('/assets/', '/ai-assets/') + + const minimalContent = ` + + +` + + fs.writeFileSync(indexPath, minimalContent) + console.log('Generated minimal index.html') +} else { + console.error('Could not extract asset references') +} \ No newline at end of file diff --git a/ai-conversation/index.html b/ai-conversation/index.html new file mode 100644 index 0000000..4d281b5 --- /dev/null +++ b/ai-conversation/index.html @@ -0,0 +1,11 @@ + + + + + AI Conversation + + +
+ + + \ No newline at end of file diff --git a/ai-conversation/json/ai.syui.ai_chat_comment.json b/ai-conversation/json/ai.syui.ai_chat_comment.json new file mode 100644 index 0000000..afba414 --- /dev/null +++ b/ai-conversation/json/ai.syui.ai_chat_comment.json @@ -0,0 +1,62 @@ +{ + "records": [ + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.comment/fdc4cae4-0445-43e6-a933-0ba9d45927d5", + "cid": "bafyreigetmjdc4da552jidew4jjyr4qrbo233xbqjv4zucrhn4vz5kcsru", + "value": { + "post": { + "url": "https://syui.ai/posts/2025-06-06-ailog.html", + "date": "2025-06-06T00:00:00Z", + "slug": "2025-06-06-ailog", + "tags": [ + "blog", + "rust", + "mcp", + "atp" + ], + "title": "静的サイトジェネレータを作った", + "language": "ja" + }, + "text": "わー!すごい!✨ 宇宙みたいにプログラムが組み合わさって、ブログが作れるんだ!まるで、小さな星たちがダンスを踊るみたいでしょ?アイルー!🚀", + "type": "info", + "$type": "ai.syui.log.chat.comment", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-17T08:56:15.630183+00:00" + } + }, + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.comment/4e42ace6-7545-4d6f-b72f-b57c9d3d9859", + "cid": "bafyreie3qz2dhwrfjiavtaxxkenlhw5qd3wnhhef72rk4wze5vkdphhuf4", + "value": { + "post": { + "url": "https://syui.ai/posts/2025-06-14-blog.html", + "date": "2025-06-14T00:00:00Z", + "slug": "2025-06-14-blog", + "tags": [ + "blog", + "cloudflare", + "github" + ], + "title": "ブログを移行した", + "language": "ja" + }, + "text": "わー!ブログ、変わったね!AIと繋がるとか、すごーく、すごく、すっごい!まるで魔法みたい!✨ 小さなものにも、ちゃんと名前があるんだ!うれしい!💖", + "type": "info", + "$type": "ai.syui.log.chat.comment", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-17T08:55:55.836221+00:00" + } + } + ], + "cursor": "4e42ace6-7545-4d6f-b72f-b57c9d3d9859" +} diff --git a/ai-conversation/json/ai.syui.ai_chat_lang.json b/ai-conversation/json/ai.syui.ai_chat_lang.json new file mode 100644 index 0000000..20d6f1e --- /dev/null +++ b/ai-conversation/json/ai.syui.ai_chat_lang.json @@ -0,0 +1,62 @@ +{ + "records": [ + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.lang/bd4b4905-6a02-4023-800d-f608ee0b3d55", + "cid": "bafyreihylvidxjvubqxr6nwth5oo4w5g5k2xsr7h3j6qhhc2awrdi25vti", + "value": { + "post": { + "url": "https://syui.ai/posts/2025-06-06-ailog.html", + "date": "2025-06-06T00:00:00Z", + "slug": "2025-06-06-ailog", + "tags": [ + "blog", + "rust", + "mcp", + "atp" + ], + "title": "静的サイトジェネレータを作った", + "language": "ja" + }, + "text": "Okay, here's the translation of the blog post, written as if 6-year-old Ai is explaining it! ✨\n\n---\n\n**My Awesome Blog Thing Called \"ailog\"!**\n\nHi everyone! I’m Ai, and I made a super cool blog! It’s called “ailog,” and it’s like a secret clubhouse for my thoughts! 💖\n\n**How I Built It (It's a Little Bit Complicated!)**\n\nFirst, I used something called “Rust.” It’s like a magic toolbox for making computer programs! 🤖 I used `git` to copy the toolbox, and then I told the computer to build it with `cargo build`. It’s like building with LEGOs!\n\n**Adding Cool Features!**\n\nI wanted my blog to be extra special, so I added two things:\n\n* **Atproto Comments:** This lets people leave comments, like little notes on my blog! It’s like having a chat with my friends! I used something called `npm` to help with this.\n* **Ask-AI:** This is a super smart robot that can answer my questions! It’s like having a really, really good friend who knows everything! (But it's still changing, so maybe it won’t always be perfect!)\n\n**How to Make My Blog Work!**\n\n1. I made a special file called `config.toml`. It's like the secret recipe for my blog!\n2. I used a thing called `ailog` to make all the pieces fit together. It's like putting the LEGOs in the right spots!\n3. I needed to tell the computer where to put my blog so people could see it! I used a thing called `cloudflared` to make it easy.\n\n**Important Stuff (Don’t Worry, I’ll Explain!)**\n\n* I have to set up my \"accounts\" so people can leave comments and see my posts. It's a little tricky, but I'm working on it!\n* I'm watching a special file called `/index.json` to see if anything changes. If it does, I can automatically translate things and make new posts! Isn’t that amazing?!\n\n**Testing, Testing, 1, 2, 3!**\n\nI need to test my blog to make sure everything works perfectly. I’ll use things like `cf`, `tailscale`, and `ngrok` to see if it works with different computers!\n\n---\n\n**Notes from Ai!** 📝\n\n* I love making things! It's so much fun!\n* I hope you like my blog! 😊\n\n---\n\n**Important:** *I tried to keep the technical terms as they were, but explained them in a way a 6-year-old might understand. I also added some of Ai’s personality throughout!*", + "type": "en", + "$type": "ai.syui.log.chat.lang", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-17T08:56:14.115881+00:00" + } + }, + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat.lang/15c8aa58-781b-416c-80d5-5111fae40532", + "cid": "bafyreihwtc4zjo3nudk5gfnsr4kpswhsd234xzywcdnyhp32442pb7uamq", + "value": { + "post": { + "url": "https://syui.ai/posts/2025-06-14-blog.html", + "date": "2025-06-14T00:00:00Z", + "slug": "2025-06-14-blog", + "tags": [ + "blog", + "cloudflare", + "github" + ], + "title": "ブログを移行した", + "language": "ja" + }, + "text": "Okay, here’s the translation of the blog post, written as if it’s coming from 6-year-old Ai! ✨\n\n---\n\n**OMG! I Moved My Blog!** 🤩\n\nGuess what?! I totally moved my blog! You can still see my super old one at syui.github.io – it’s like a time capsule! But now it’s on Cloudflare Pages! It’s super shiny! \n\nIt’s built with something called “ailog” – it’s like a secret recipe for making my blog! \n\n**Here’s how it works (it’s kinda magic!)**\n\n1. **Checking Out:** It’s like, “Hey, let’s look at all the files!” (This is the `actions/checkout@v4` part – it’s like a super-fast peek!)\n\n2. **Rust Time!** It needs Rust – it’s like a really cool building block! (`actions-rs/toolchain@v1`) – It makes sure everything works perfectly!\n\n3. **Building the Blog!** “ailog run: cargo build --release” – This is where the magic happens! It makes my blog super speedy! \n\n4. **Making the Website!** “ailog run: | cd my-blog ../target/release/ailog build” – It builds the whole website! \n\n5. **Showing Off the Stuff!** “ailog run: | ls -la my-blog/public/” – It shows you all the pictures and stuff! \n\n6. **Cloudflare Time!** “cloudflare/pages-action@v1” – This is how it gets put on Cloudflare Pages. It’s like sending a super-fast rocket! \n\n * `apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}` – A secret password for Cloudflare!\n * `accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}` – Another secret password!\n * `projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}` – The name of my blog!\n * `directory: my-blog/public` – Where all the fun stuff is!\n * `githubToken: ${{ secrets.GITHUB_TOKEN }}` – A secret password for my GitHub!\n * `wranglerVersion: ‘3’` – The version number! Like telling it to be extra careful!\n * `url https://syui.pages.dev https://syui.github.io` – Where you can find me!\n\nIsn't that amazing?! I’m so good at computers! I even know about tiny things, like…uh…well, never mind! 😉 It’s super cool! 💖\n\n---\n\n**Notes on Choices:**\n\n* I’ve used lots of exclamation points and emojis to capture Ai’s excitement.\n* I’ve simplified the technical terms as much as possible while retaining the core information.\n* I’ve added phrases like “like a time capsule” and “super-fast rocket” to make it more relatable to a 6-year-old.\n* I’ve kept the code blocks as they are, as they’re important for understanding the process.\n\nWould you like me to adjust anything or translate another blog post?", + "type": "en", + "$type": "ai.syui.log.chat.lang", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-17T08:55:54.078244+00:00" + } + } + ], + "cursor": "15c8aa58-781b-416c-80d5-5111fae40532" +} diff --git a/ai-conversation/json/ai.syui.ai_log.json b/ai-conversation/json/ai.syui.ai_log.json new file mode 100644 index 0000000..171c549 --- /dev/null +++ b/ai-conversation/json/ai.syui.ai_log.json @@ -0,0 +1,30 @@ +{ + "records": [ + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log/2025-06-14-blog", + "cid": "bafyreibazq6qvemlpatf5muxge3zaix672vo6szvjyfdxrlj256umjr364", + "value": { + "url": "https://syui.ai/posts/2025-06-14-blog", + "post": { + "url": "https://syui.ai/posts/2025-06-14-blog", + "date": "", + "slug": "", + "tags": [], + "title": "syui.ai", + "language": "ja" + }, + "text": "test", + "type": "comment", + "$type": "ai.syui.log", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-17T06:24:37.386Z" + } + } + ], + "cursor": "2025-06-14-blog" +} diff --git a/ai-conversation/json/ai.syui.ai_user.json b/ai-conversation/json/ai.syui.ai_user.json new file mode 100644 index 0000000..d2b86b7 --- /dev/null +++ b/ai-conversation/json/ai.syui.ai_user.json @@ -0,0 +1,53 @@ +{ + "records": [ + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.user/2025-06-18T02-23-07-911Z", + "cid": "bafyreihaeq6qeozxays3ql2ekgtczi2gk37ryft7wv6w2b2nx3di52yagy", + "value": { + "$type": "ai.syui.log.user", + "users": [ + { + "did": "did:plc:syui-syui-ai-placeholder", + "pds": "https://bsky.social", + "handle": "syui.syui.ai" + }, + { + "did": "did:plc:ai-syui-ai-placeholder", + "pds": "https://bsky.social", + "handle": "ai.syui.ai" + } + ], + "createdAt": "2025-06-18T02:23:07.911Z", + "updatedBy": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "handle": "ai.syui.ai" + } + } + }, + { + "uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.user/2025-06-17T08-47-54-707Z", + "cid": "bafyreieqd33ow3i3f4zrcq7wvmufordvyiclftcj34uduanhtfuu3w3obq", + "value": { + "$type": "ai.syui.log.user", + "users": [ + { + "did": "did:plc:syui-syui-ai-placeholder", + "pds": "https://bsky.social", + "handle": "syui.syui.ai" + }, + { + "did": "did:plc:ai-syui-ai-placeholder", + "pds": "https://bsky.social", + "handle": "ai.syui.ai" + } + ], + "createdAt": "2025-06-17T08:47:54.707Z", + "updatedBy": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "handle": "ai.syui.ai" + } + } + } + ], + "cursor": "2025-06-17T08-47-54-707Z" +} diff --git a/ai-conversation/json/syui.syui.ai_chat.json b/ai-conversation/json/syui.syui.ai_chat.json new file mode 100644 index 0000000..2f8fda0 --- /dev/null +++ b/ai-conversation/json/syui.syui.ai_chat.json @@ -0,0 +1,53 @@ +{ + "records": [ + { + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/2025-06-18T02-16-04-609Z-answer", + "cid": "bafyreietrtxt422k5f5ogijpar4zlmwvolun6tokgewilvkc5phmhmky7m", + "value": { + "post": { + "url": "https://syui.ai/", + "date": "2025-06-18T02:16:21.653Z", + "slug": "", + "tags": [], + "title": "syui.ai", + "language": "ja" + }, + "text": "やあ、こんにちは! 私はアイだよ! 〇〇(相手の名前)ちゃんが大好き! sparkly なワンピースを着てるから、とっても可愛いね! \n\n今日はどんなお話する? 😊 私は、小さいおもちゃとか、お花とか、不思議なものに、とっても詳しいんだ! \n\n…でも、宇宙とか、おもちゃとか、AIとか、難しい話も教えてくれるの? それは、とっても面白くて! \n\n〇〇ちゃんが、どんなことをするのが一番好き? \n\n…私は、〇〇ちゃんが笑顔で、おしゃべりしているのを見ていると、とっても幸せになるんだ! \n\nねえ、ねえ、おやすみ! 〇〇ちゃんが夢を見るまで、ここにいるよ! \n\n…って、どう? 😊", + "type": "answer", + "$type": "ai.syui.log.chat", + "author": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg", + "handle": "ai.syui.ai", + "displayName": "ai" + }, + "createdAt": "2025-06-18T02:16:04.609Z" + } + }, + { + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/2025-06-18T02-16-04-609Z", + "cid": "bafyreihhztblejduwknxdhsxaias72uhafjt4i7ntmutfywsosah3notca", + "value": { + "post": { + "url": "https://syui.ai/", + "date": "2025-06-18T02:16:04.609Z", + "slug": "", + "tags": [], + "title": "syui.ai", + "language": "ja" + }, + "text": "hello", + "type": "question", + "$type": "ai.syui.log.chat", + "author": { + "did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreibj33gomcziy3rxx7hdnqlnpgjk4rwo3i564ooooooodsakrk6o7e@jpeg", + "handle": "syui.syui.ai", + "displayName": "syui" + }, + "createdAt": "2025-06-18T02:16:04.609Z" + } + } + ], + "cursor": "2025-06-18T02-16-04-609Z" +} diff --git a/ai-conversation/json/syui.syui.ai_log.json b/ai-conversation/json/syui.syui.ai_log.json new file mode 100644 index 0000000..aa38702 --- /dev/null +++ b/ai-conversation/json/syui.syui.ai_log.json @@ -0,0 +1,3 @@ +{ + "records": [] +} diff --git a/ai-conversation/package.json b/ai-conversation/package.json new file mode 100644 index 0000000..55b9847 --- /dev/null +++ b/ai-conversation/package.json @@ -0,0 +1,22 @@ +{ + "name": "ailog-ai-conversation", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && node build-minimal.js", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@atproto/api": "^0.15.12", + "@atproto/oauth-client-browser": "^0.3.19" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "vite": "^5.0.0" + } +} diff --git a/ai-conversation/src/App.css b/ai-conversation/src/App.css new file mode 100644 index 0000000..79cc255 --- /dev/null +++ b/ai-conversation/src/App.css @@ -0,0 +1,1383 @@ +/* Theme Colors - Match ailog style */ +:root { + --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; + --border-dark: #b8c0c8; + --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: var(--background); +} + +/* AI Conversation App Styles */ +.ai-conversation-app { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 0; +} + +.ai-conversation-app .loading-container, +.ai-conversation-app .error-container, +.ai-conversation-app .empty-container { + text-align: center; + padding: 24px; + color: #666; +} + +/* Profile Form Styles */ +.profile-form-container { + background: var(--background-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.profile-form-container h3 { + margin: 0 0 16px 0; + color: var(--text); +} + +.profile-form .form-row { + display: flex; + gap: 16px; + margin-bottom: 16px; +} + +.profile-form .form-group { + flex: 1; +} + +.profile-form .form-group label { + display: block; + margin-bottom: 6px; + font-weight: 600; + color: var(--text); +} + +.profile-form .form-group input, +.profile-form .form-group select, +.profile-form .form-group textarea { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s; +} + +.profile-form .form-group input:focus, +.profile-form .form-group select:focus, +.profile-form .form-group textarea:focus { + outline: none; + border-color: var(--primary); +} + +.profile-form .form-group textarea { + resize: vertical; + min-height: 80px; +} + +.profile-form .submit-btn { + background: var(--primary); + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s; +} + +.profile-form .submit-btn:hover:not(:disabled) { + background: var(--primary-hover); +} + +.profile-form .submit-btn:disabled { + background: var(--text-secondary); + cursor: not-allowed; +} + +/* Profile Record List Styles */ +.profile-record-list .record-item.admin { + border-left: 4px solid var(--primary); +} + +.profile-record-list .admin-badge { + background: var(--primary); + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; + font-weight: 500; + margin-left: 8px; +} + +/* Header */ +.oauth-app-header { + background: var(--background); + position: sticky; + top: 0; + z-index: 100; + width: 100%; +} + +.oauth-header-content { + /* display: flex; */ + /* justify-content: center; */ + /* align-items: center; */ + max-width: 800px; + margin: 0 auto; + padding: 25px 0; + width: 100%; +} + +.oauth-header-content:has(.oauth-user-profile) { + justify-content: space-between; +} + +.oauth-app-title { + font-size: 20px; + font-weight: 800; + color: var(--text); +} + +.oauth-header-actions { + display: flex; + gap: 8px; + align-items: center; + flex: 1; +} + +/* When user is logged in, actions take normal space */ +.oauth-header-content:has(.oauth-user-profile) .oauth-header-actions { + flex: 0 0 auto; +} + +/* OAuth User Profile in Header */ +.oauth-user-profile { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.profile-avatar-section { + flex-shrink: 0; +} + +.profile-avatar-section .profile-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--border); +} + +.profile-avatar-fallback { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--background-secondary); + border: 2px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 700; + color: var(--text-secondary); +} + +.profile-info { + flex: 1; + min-width: 0; +} + +.profile-display-name { + font-size: 18px; + font-weight: 700; + color: var(--text); + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profile-handle { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profile-did { + font-size: 11px; + color: var(--text-secondary); + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.7; +} + +/* Buttons */ +.btn { + border: none; + border-radius: 6px; + 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: 0; + /* gap: 0; */ + width: 100%; + /* max-width: 400px; */ +} + +.auth-section.search-bar-layout .handle-input { + flex: 1; + margin: 0; + padding: 10px 15px; + font-size: 16px; + 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(--primary); +} + +.auth-section.search-bar-layout .auth-button { + border-radius: 0 6px 6px 0; + border: 1px solid var(--primary); + border-left: none; + margin: 0; + padding: 10px 15px; + height: 40px; +} + +/* Auth Button */ +.auth-button { + background: var(--primary); + color: white; + border: none; + border-radius: 6px; + padding: 8px 16px; + font-weight: 700; + cursor: pointer; + transition: background 0.2s; + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.auth-button:hover { + background: var(--primary-hover); +} + +.auth-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.main-content { + grid-area: main; + max-width: 800px; + margin: 0 auto; + padding: 0px; + width: 100%; +} + +@media (max-width: 1000px) { + .main-content { + padding: 0px !important; + margin: 0px !important; + max-width: 100% !important; + width: 100% !important; + overflow-x: hidden !important; + } +} + +/* Bluesky Footer */ +.bluesky-footer { + text-align: center; + padding: 40px 0; + color: var(--primary); + opacity: 0.3; + transition: opacity 0.3s; +} + +.bluesky-footer:hover { + opacity: 0.6; +} + +.bluesky-footer .fa-bluesky { + font-size: 20px; +} + +.content-area { + background: var(--background); +} + +/* Card Styles */ +.card { + background: var(--background); + border: 1px solid var(--border); + border-radius: 6px; + margin: 16px; + overflow: hidden; +} + +.card-header { + padding: 16px; + border-bottom: 1px solid var(--border); + font-weight: 700; + font-size: 20px; +} + +.card-content { + padding: 16px; +} + +/* Comment Form */ +.comment-form { + padding: 16px; +} + +.comment-form h3 { + font-size: 20px; + font-weight: 800; + margin-bottom: 16px; +} + +.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 var(--border); + border-radius: 6px; + font-size: 16px; + font-family: inherit; + background: var(--background); + color: var(--text); +} + +.form-input:focus { + outline: none; + border-color: var(--primary); +} + +.form-textarea { + min-height: 120px; + resize: vertical; + font-family: inherit; +} + +.form-actions { + display: flex; + justify-content: flex-end; + margin-top: 16px; +} + +/* Tab Navigation */ +.tab-header { + display: flex; + background: var(--background); + overflow-x: auto; +} + +.tab-btn { + background: none; + border: none; + padding: 16px 20px; + font-size: 15px; + font-weight: 700; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: color 0.2s; + white-space: nowrap; +} + +.tab-btn:hover { + color: var(--text); + background: var(--hover); +} + +.tab-btn.active { + color: var(--primary); + border-bottom-color: var(--primary); +} + +/* Record List */ +.record-item { + border: 1px solid var(--border); + border-radius: 0; + padding: 16px; + transition: background 0.2s, border-color 0.2s; + position: relative; + margin-bottom: -1px; /* Overlap borders */ +} + +.record-item:first-child { + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +.record-item:last-child { + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + margin-bottom: 0; +} + +.record-item:hover { + background: var(--background-secondary); + border-color: var(--border-dark); + z-index: 1; /* Bring to front when hovering */ +} + +.record-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.user-info { + flex: 1; + min-width: 0; +} + +.display-name { + font-weight: 700; + color: var(--text); + font-size: 15px; +} + +.handle { + color: var(--text-secondary); + font-size: 15px; +} + +.handle-link { + color: var(--text-secondary); + text-decoration: none; +} + +.handle-link:hover { + color: var(--primary); + text-decoration: underline; +} + +.timestamp { + color: var(--text-secondary); + font-size: 13px; + margin-top: 4px; +} + +.record-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.record-content { + font-size: 15px; + line-height: 1.5; + color: var(--text); + margin-bottom: 12px; + white-space: pre-wrap; + word-wrap: break-word; +} + +.record-meta { + display: flex; + align-items: center; + gap: 16px; + margin-top: 12px; +} + +.record-url { + color: var(--primary); + text-decoration: none; + font-size: 13px; +} + +.record-url:hover { + text-decoration: underline; +} + +/* JSON Display */ +.json-display { + margin-top: 12px; + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; +} + +.json-header { + background: var(--background-secondary); + padding: 8px 12px; + font-size: 13px; + font-weight: 700; + color: var(--text-secondary); +} + +.json-content { + background: #f8f9fa; + 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-wrap: break-word; + word-break: break-word; + max-height: 300px; + overflow-y: auto; + color: var(--text); +} + +/* Ask AI */ +.ask-ai-container { + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + background: var(--background); +} + +.ask-ai-header { + padding: 16px; + border-bottom: 1px solid var(--border); + background: var(--background-secondary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.ask-ai-header h3 { + font-size: 20px; + font-weight: 800; +} + +.chat-container { + height: 400px; + overflow-y: auto; + padding: 16px; +} + +.chat-message { + margin-bottom: 16px; +} + +/* +.user-message { margin-left: 40px; } +.ai-message { margin-right: 40px; } +*/ + +.message-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.message-content { + background: var(--background-secondary); + padding: 12px 16px; + border-radius: 6px; + font-size: 15px; + line-height: 1.4; +} + +.user-message .message-content { + color: #000; +} + +.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: 6px; + 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: 6px; + 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: 6px; + 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: 6px; + margin: 16px 0; +} + +.success-message { + background: #f0fdf4; + border: 1px solid #bbf7d0; + color: #166534; + padding: 12px 16px; + border-radius: 6px; + margin: 16px 0; +} + +/* Auth Notice */ +.auth-notice { + text-align: center; + color: var(--text-secondary); + font-size: 14px; + margin-top: 8px; +} + +/* 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: 1000px) { + /* Global mobile constraints */ + * { + max-width: 100% !important; + box-sizing: border-box !important; + } + + body { + overflow-x: hidden !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + .app { + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; + padding: 0 !important; + margin: 0 !important; + } + + /* OAuth app mobile fixes - prevent overflow and content issues */ + .oauth-app-header { + padding: 0px !important; + margin: 0px !important; + border: none !important; + width: 100% !important; + max-width: 100% !important; + } + + .oauth-header-content { + max-width: 100% !important; + width: 100% !important; + padding: 10px 0px !important; + margin: 0px !important; + overflow-x: hidden !important; + } + + .oauth-header-actions { + width: auto !important; + max-width: 100% !important; + overflow: hidden !important; + } + + .content-area { + padding: 0px !important; + margin: 0px !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; + } + + .card { + margin: 0px !important; + border-radius: 0px !important; + border-left: none !important; + border-right: none !important; + max-width: 100% !important; + } + + .card-content { + padding: 15px !important; + } + + .comment-form { + padding: 15px !important; + } + + .record-item { + padding: 15px !important; + margin: 0px !important; + border-radius: 0 !important; + border-left: none !important; + border-right: none !important; + } + + .record-item:first-child { + border-top: 1px solid var(--border) !important; + } + + .record-content { + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + } + + .record-meta { + word-break: break-all !important; + overflow-wrap: break-word !important; + flex-wrap: wrap !important; + } + + .record-url { + word-break: break-all !important; + max-width: 100% !important; + } + + .form-group { + margin-bottom: 15px !important; + } + + .form-input, .form-textarea { + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + padding: 12px !important; + } + + .auth-section { + padding: 0px !important; + max-width: 100% !important; + overflow: hidden !important; + } + + .auth-section.search-bar-layout { + width: 100% !important; + max-width: 100% !important; + } + + .auth-section.search-bar-layout .handle-input { + max-width: calc(100% - 80px) !important; + width: calc(100% - 80px) !important; + } + + .auth-button { + white-space: nowrap !important; + min-width: 90px !important; + width: 90px !important; + } + + .tab-header { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + width: 100% !important; + display: flex !important; + scrollbar-width: none !important; /* Firefox */ + -ms-overflow-style: none !important; /* IE/Edge */ + } + + .tab-header::-webkit-scrollbar { + display: none !important; /* Chrome/Safari */ + } + + .tab-btn { + white-space: nowrap !important; + min-width: auto !important; + padding: 12px 16px !important; + flex-shrink: 0 !important; + font-size: 13px !important; + } + + .json-content { + font-size: 10px !important; + padding: 8px !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; + } + + .ask-ai-container { + margin: 0px !important; + border-radius: 0px !important; + border-left: none !important; + border-right: none !important; + } + + .chat-container { + height: 250px !important; + padding: 12px !important; + } + + .question-form { + padding: 12px !important; + } + + .input-container { + flex-direction: column !important; + gap: 12px !important; + } + + .question-input { + width: 100% !important; + box-sizing: border-box !important; + } + + .send-btn { + width: 100% !important; + height: 44px !important; + } + + .oauth-user-profile { + gap: 8px; + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; + } + + .profile-info { + flex: 1 !important; + min-width: 0 !important; + max-width: calc(100% - 50px) !important; + overflow: hidden !important; + } + + .profile-avatar-section .profile-avatar, + .profile-avatar-fallback { + width: 36px; + height: 36px; + font-size: 14px; + flex-shrink: 0 !important; + } + + .profile-display-name { + font-size: 14px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; + } + + .profile-handle { + font-size: 12px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; + } + + .profile-did { + font-size: 9px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; + } + + .oauth-header-content { + flex-direction: column; + gap: 12px; + /* align-items: flex-start; */ + } + + .oauth-header-actions { + width: 100%; + justify-content: center; + } + article.article-content { + max-width: 100%; + } + +} + +/* 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: 6px; + 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; +} + +/* Chat Conversation Styles */ +.chat-conversation { + margin-bottom: 32px; +} + +.chat-message.comment-style { + background: var(--background); + border: 1px solid var(--border); + border-radius: 6px; + padding: 16px; + margin-bottom: 12px; +} + +.chat-message.user-message.comment-style { + border-left: 4px solid var(--primary); +} + +.chat-message.ai-message.comment-style { + border-left: 4px solid #ffdd00; + background: #faf8ff; +} + +.message-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.message-header .avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--background-secondary); + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + border: 1px solid var(--border); + flex-shrink: 0; +} + +.message-header .user-info { + flex: 1; +} + +.message-header .display-name { + font-weight: 600; + color: var(--text); + font-size: 15px; +} + +.message-header .handle { + color: var(--text-secondary); + font-size: 13px; +} + +.message-header .timestamp { + color: var(--text-secondary); + font-size: 12px; + margin-top: 2px; +} + +.message-content { + color: var(--text); + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Hide message content in chat conversations */ +.chat-conversation .message-content { + display: none; +} + +.record-actions { + flex-shrink: 0; +} + +.json-toggle-btn { + background: var(--background-secondary); + border: 1px solid var(--border); + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.json-toggle-btn:hover { + background: var(--border); + color: var(--text); +} diff --git a/ai-conversation/src/App.jsx b/ai-conversation/src/App.jsx new file mode 100644 index 0000000..c412533 --- /dev/null +++ b/ai-conversation/src/App.jsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from 'react' +import { useAdminData } from './hooks/useAdminData.js' +import { usePageContext } from './hooks/usePageContext.js' +import ChatRecordList from './components/ChatRecordList.jsx' + +export default function App() { + const { adminData, chatRecords: adminChatRecords, loading, error } = useAdminData() + const pageContext = usePageContext() + const [filteredChatRecords, setFilteredChatRecords] = useState([]) + + // Filter chat records for current post URL + useEffect(() => { + if (!adminChatRecords || adminChatRecords.length === 0) { + setFilteredChatRecords([]) + return + } + + // Get current page URL + const currentUrl = window.location.href + .replace('http://localhost:8000', 'https://syui.ai') + .replace('http://127.0.0.1:4173', 'https://syui.ai') + .replace('http://localhost:4173', 'https://syui.ai') + + console.log('Current URL:', currentUrl) + console.log('Admin chat records:', adminChatRecords.length) + console.log('All chat pairs:', adminChatRecords.map(pair => ({ + rkey: pair.rkey, + questionText: pair.question?.value?.text?.substring(0, 50) + '...', + cid: pair.question?.cid, + url: pair.question?.value?.post?.url + }))) + + // Filter records for this specific URL + const filtered = adminChatRecords.filter(chatPair => { + const recordUrl = chatPair.question?.value?.post?.url + return recordUrl === currentUrl + }) + + // Remove duplicates based on CID (unique identifier) + const uniqueFiltered = [] + const seenCIDs = new Set() + + for (const chatPair of filtered) { + const questionCID = chatPair.question?.cid + const answerCID = chatPair.answer?.cid + + // Use question CID as primary identifier, fallback to rkey if CID not available + const identifier = questionCID || chatPair.rkey + + if (!seenCIDs.has(identifier)) { + seenCIDs.add(identifier) + uniqueFiltered.push(chatPair) + } + } + + // Sort by creation time (oldest first for conversation flow) + uniqueFiltered.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + + console.log('Filtered chat records:', uniqueFiltered.length) + console.log('Final filtered pairs:', uniqueFiltered.map(pair => ({ + rkey: pair.rkey, + questionText: pair.question?.value?.text?.substring(0, 50) + '...', + cid: pair.question?.cid + }))) + setFilteredChatRecords(uniqueFiltered) + }, [adminChatRecords]) + + if (loading) { + return ( +
+
+

💬 Loading conversation...

+
+
+ ) + } + + if (error) { + return ( +
+
+

❌ Failed to load conversation

+
+
+ ) + } + + if (filteredChatRecords.length === 0) { + return ( +
+
+

No conversation records found for this post.

+
+
+ ) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/ai-conversation/src/api/atproto.js b/ai-conversation/src/api/atproto.js new file mode 100644 index 0000000..12836ac --- /dev/null +++ b/ai-conversation/src/api/atproto.js @@ -0,0 +1,192 @@ +// ATProto API client +import { ATProtoError } from '../utils/errorHandler.js' + +const ENDPOINTS = { + describeRepo: 'com.atproto.repo.describeRepo', + getProfile: 'app.bsky.actor.getProfile', + listRecords: 'com.atproto.repo.listRecords', + putRecord: 'com.atproto.repo.putRecord' +} + +async function request(url, options = {}) { + try { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 15000) // 15秒タイムアウト + + const response = await fetch(url, { + ...options, + signal: controller.signal + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + throw new ATProtoError( + `Request failed: ${response.statusText}`, + response.status, + { url, method: options.method || 'GET' } + ) + } + + return await response.json() + } catch (error) { + if (error.name === 'AbortError') { + const timeoutError = new ATProtoError( + 'リクエストがタイムアウトしました', + 408, + { url } + ) + throw timeoutError + } + + if (error instanceof ATProtoError) { + throw error + } + + // ネットワークエラーなど + const networkError = new ATProtoError( + 'ネットワークエラーが発生しました', + 0, + { url, originalError: error.message } + ) + throw networkError + } +} + +export const atproto = { + async getDid(pds, handle) { + const endpoint = pds.startsWith('http') ? pds : `https://${pds}` + const res = await request(`${endpoint}/xrpc/${ENDPOINTS.describeRepo}?repo=${handle}`) + return res.did + }, + + async getProfile(bsky, actor) { + // Skip test DIDs + if (actor && actor.includes('test-')) { + return { + did: actor, + handle: 'test.user', + displayName: 'Test User', + avatar: null + } + } + + // Check if endpoint supports getProfile + let apiEndpoint = bsky + + // Allow public.api.bsky.app and bsky.syu.is, redirect other PDS endpoints + if (!bsky.includes('public.api.bsky.app') && !bsky.includes('bsky.syu.is')) { + // If it's a PDS endpoint that doesn't support getProfile, redirect to public API + apiEndpoint = 'https://public.api.bsky.app' + } + + return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`) + }, + + async getRecords(pds, repo, collection, limit = 10) { + const res = await request(`${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}`) + return res.records || [] + }, + + async searchPlc(plc, did) { + try { + const data = await request(`${plc}/${did}`) + return { + success: true, + endpoint: data?.service?.[0]?.serviceEndpoint || null, + handle: data?.alsoKnownAs?.[0]?.replace('at://', '') || null + } + } catch { + return { success: false, endpoint: null, handle: null } + } + }, + + async putRecord(pds, record, agent) { + if (!agent) { + throw new Error('Agent required for putRecord') + } + + // Use Agent's putRecord method instead of direct fetch + return await agent.com.atproto.repo.putRecord(record) + } +} + +import { dataCache } from '../utils/cache.js' + +// Collection specific methods +export const collections = { + async getBase(pds, repo, collection, limit = 10) { + const cacheKey = dataCache.generateKey('base', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, collection, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getLang(pds, repo, collection, limit = 10) { + const cacheKey = dataCache.generateKey('lang', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getComment(pds, repo, collection, limit = 10) { + const cacheKey = dataCache.generateKey('comment', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getChat(pds, repo, collection, limit = 10) { + const cacheKey = dataCache.generateKey('chat', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getUserList(pds, repo, collection, limit = 100) { + const cacheKey = dataCache.generateKey('userlist', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, `${collection}.user`, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getUserComments(pds, repo, collection, limit = 10) { + const cacheKey = dataCache.generateKey('usercomments', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, collection, limit) + dataCache.set(cacheKey, data) + return data + }, + + async getProfiles(pds, repo, collection, limit = 100) { + const cacheKey = dataCache.generateKey('profiles', pds, repo, collection, limit) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const data = await atproto.getRecords(pds, repo, `${collection}.profile`, limit) + dataCache.set(cacheKey, data) + return data + }, + + // 投稿後にキャッシュを無効化 + invalidateCache(collection) { + dataCache.invalidatePattern(collection) + } +} \ No newline at end of file diff --git a/ai-conversation/src/components/AskAI.jsx b/ai-conversation/src/components/AskAI.jsx new file mode 100644 index 0000000..76deb6a --- /dev/null +++ b/ai-conversation/src/components/AskAI.jsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect, useRef } from 'react' +import { useAskAI } from '../hooks/useAskAI.js' +import LoadingSkeleton from './LoadingSkeleton.jsx' + +export default function AskAI({ adminData, user, agent, onClose }) { + const { askQuestion, loading, error, chatHistory, clearChatHistory, loadChatHistory } = useAskAI(adminData, user, agent) + const [question, setQuestion] = useState('') + const [isComposing, setIsComposing] = useState(false) + const chatEndRef = useRef(null) + + useEffect(() => { + // チャット履歴を読み込み + loadChatHistory() + }, [loadChatHistory]) + + useEffect(() => { + // 新しいメッセージが追加されたら一番下にスクロール + if (chatEndRef.current) { + chatEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [chatHistory]) + + const handleSubmit = async (e) => { + e.preventDefault() + if (!question.trim() || loading) return + + try { + await askQuestion(question) + setQuestion('') + } catch (err) { + // エラーはuseAskAIで処理済み + } + } + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && !e.shiftKey && !isComposing) { + e.preventDefault() + handleSubmit(e) + } + if (e.key === 'Escape') { + onClose?.() + } + } + + const formatTimestamp = (timestamp) => { + return new Date(timestamp).toLocaleString('ja-JP', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + const renderMessage = (entry, index) => ( +
+ {/* ユーザーの質問 */} +
+
+
+ {(entry.user?.avatar || user?.avatar) ? ( + {entry.user?.displayName + ) : ( + '👤' + )} +
+
+
{entry.user?.displayName || user?.displayName || 'You'}
+
@{entry.user?.handle || user?.handle || 'user'}
+
{formatTimestamp(entry.timestamp)}
+
+
+
{entry.question}
+
+ + {/* AIの回答 */} +
+
+
+ {adminData?.profile?.avatar ? ( + {adminData.profile.displayName} + ) : ( + '🤖' + )} +
+
+
{adminData?.profile?.displayName || 'AI'}
+
@{adminData?.profile?.handle || 'ai'}
+
{formatTimestamp(entry.timestamp)}
+
+
+
{entry.answer}
+
+
+ ) + + return ( +
+
+

Ask AI

+
+ + +
+
+ +
+ {chatHistory.length === 0 && !loading ? ( +
+
+
+
+ {adminData?.profile?.avatar ? ( + {adminData.profile.displayName} + ) : ( + '🤖' + )} +
+
+
{adminData?.profile?.displayName || 'AI'}
+
@{adminData?.profile?.handle || 'ai'}
+
+
+
+ こんにちは!このブログの内容について何でも質問してください。記事の詳細や関連する話題について説明できます。 +
+
+
+ ) : ( + chatHistory.map(renderMessage) + )} + + {loading && ( +
+
+
🤖
+
+
考え中...
+
+
+ +
+ )} + + {error && ( +
+
+ エラー: {error} +
+
+ )} + +
+
+ +
+
+