diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b351d71..d9c03cf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,8 @@ "Bash(git push:*)", "Bash(git tag:*)", "Bash(../bin/ailog:*)", - "Bash(../target/release/ailog oauth build:*)" + "Bash(../target/release/ailog oauth build:*)", + "Bash(ailog:*)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index 218408e..0752df2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ node_modules package-lock.json my-blog/static/assets/comment-atproto-* bin/ailog +docs diff --git a/my-blog/static/index.html b/my-blog/static/index.html index 41ee2db..b9b2aeb 100644 --- a/my-blog/static/index.html +++ b/my-blog/static/index.html @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/my-blog/templates/oauth-assets.html b/my-blog/templates/oauth-assets.html index 41ee2db..b9b2aeb 100644 --- a/my-blog/templates/oauth-assets.html +++ b/my-blog/templates/oauth-assets.html @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/oauth/.env.production b/oauth/.env.production index fbe2758..8407792 100644 --- a/oauth/.env.production +++ b/oauth/.env.production @@ -4,18 +4,9 @@ VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn -# Base collection for OAuth app and ailog (all others are derived) +# Base collection (all others are derived via getCollectionNames) VITE_OAUTH_COLLECTION=ai.syui.log -# [user, chat, chat.lang, chat.comment] - -# AI Configuration -VITE_AI_ENABLED=true -VITE_AI_ASK_AI=true -VITE_AI_PROVIDER=ollama -VITE_AI_MODEL=gemma3:4b -VITE_AI_HOST=https://ollama.syui.ai -VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" -VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef # API Configuration VITE_BSKY_PUBLIC_API=https://public.api.bsky.app +VITE_ATPROTO_API=https://bsky.social diff --git a/oauth/src/App.tsx b/oauth/src/App.tsx index fb4c80b..943ed5d 100644 --- a/oauth/src/App.tsx +++ b/oauth/src/App.tsx @@ -7,32 +7,10 @@ import { appConfig, getCollectionNames } from './config/app'; import './App.css'; function App() { - console.log('APP COMPONENT LOADED - Console working!'); - console.log('Current timestamp:', new Date().toISOString()); - - // Immediately log URL information on every page load - console.log('IMMEDIATE URL CHECK:'); - console.log('- href:', window.location.href); - console.log('- pathname:', window.location.pathname); - console.log('- search:', window.location.search); - console.log('- hash:', window.location.hash); - - // Also show URL info via alert if it contains OAuth parameters + // 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); - console.log('OAuth callback URL detected!'); - } else { - // Check if we have stored OAuth info from previous steps - const preOAuthUrl = sessionStorage.getItem('pre_oauth_url'); - const storedState = sessionStorage.getItem('oauth_state'); - const storedCodeVerifier = sessionStorage.getItem('oauth_code_verifier'); - - console.log('=== OAUTH SESSION STORAGE CHECK ==='); - console.log('Pre-OAuth URL:', preOAuthUrl); - console.log('Stored state:', storedState); - console.log('Stored code verifier:', storedCodeVerifier ? 'Present' : 'Missing'); - console.log('=== END SESSION STORAGE CHECK ==='); } const [user, setUser] = useState(null); @@ -59,7 +37,6 @@ function App() { const collections = getCollectionNames(appConfig.collections.base); ws.onopen = () => { - console.log('Jetstream connected'); ws.send(JSON.stringify({ wantedCollections: [collections.comment] })); @@ -69,22 +46,20 @@ function App() { try { const data = JSON.parse(event.data); if (data.collection === collections.comment && data.commit?.operation === 'create') { - console.log('New comment detected via Jetstream:', data); // Optionally reload comments // loadAllComments(window.location.href); } } catch (err) { - console.warn('Failed to parse Jetstream message:', err); + // Ignore parsing errors } }; ws.onerror = (err) => { - console.warn('Jetstream error:', err); + // Ignore Jetstream errors }; return ws; } catch (err) { - console.warn('Failed to setup Jetstream:', err); return null; } }; @@ -108,11 +83,11 @@ function App() { // キャッシュがなければ、ATProtoから取得(認証状態に関係なく) if (!loadCachedComments()) { - console.log('No cached comments found, loading from ATProto...'); loadAllComments(); // URLフィルタリングを無効にして全コメント表示 - } else { - console.log('Cached comments loaded successfully'); } + + // Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示) + loadAiChatHistory(); // Handle popstate events for mock OAuth flow const handlePopState = () => { @@ -138,12 +113,9 @@ function App() { // Check existing sessions const checkAuth = async () => { // First check OAuth session using official BrowserOAuthClient - console.log('Checking OAuth session...'); const oauthResult = await atprotoOAuthService.checkSession(); - console.log('OAuth checkSession result:', oauthResult); if (oauthResult) { - console.log('OAuth session found:', oauthResult); // Ensure handle is not DID const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle; @@ -153,11 +125,10 @@ function App() { // Load all comments for display (this will be the default view) // Temporarily disable URL filtering to see all comments - console.log('OAuth session found, loading all comments...'); loadAllComments(); // Load AI chat history - loadAiChatHistory(userProfile.did); + loadAiChatHistory(); // Load user list records if admin if (userProfile.did === appConfig.adminDid) { @@ -166,8 +137,6 @@ function App() { setIsLoading(false); return; - } else { - console.log('No OAuth session found'); } // Fallback to legacy auth @@ -177,7 +146,6 @@ function App() { // Load all comments for display (this will be the default view) // Temporarily disable URL filtering to see all comments - console.log('Legacy auth session found, loading all comments...'); loadAllComments(); // Load user list records if admin @@ -188,7 +156,6 @@ function App() { setIsLoading(false); // 認証状態に関係なく、コメントを読み込む - console.log('No auth session found, loading all comments anyway...'); loadAllComments(); }; @@ -215,7 +182,7 @@ function App() { }; } } catch (error) { - console.error('Failed to get user profile:', error); + // Failed to get user profile } // Fallback to basic user info @@ -236,27 +203,46 @@ function App() { return `data:image/svg+xml;base64,${btoa(svg)}`; }; - const loadAiChatHistory = async (did: string) => { + const loadAiChatHistory = async () => { try { - console.log('Loading AI chat history for DID:', did); - const agent = atprotoOAuthService.getAgent(); - if (!agent) { - console.log('No agent available'); + // Load all chat records from admin's user list records + const adminDid = appConfig.adminDid; + const atprotoApi = appConfig.atprotoApi || 'https://bsky.social'; + const collections = getCollectionNames(appConfig.collections.base); + + // First, get user list from admin + const userListResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`); + + if (!userListResponse.ok) { + setAiChatHistory([]); return; } - - // Get AI chat records from current user - const response = await agent.api.com.atproto.repo.listRecords({ - repo: did, - collection: appConfig.collections.chat, - limit: 100, - }); - - console.log('AI chat history loaded:', response.data); - const chatRecords = response.data.records || []; + + const userListData = await userListResponse.json(); + const userRecords = userListData.records || []; + + // Extract unique DIDs from user records + const userDids = [...new Set(userRecords.map(record => record.value.did).filter(Boolean))]; + + // Load chat records from all registered users + const allChatRecords = []; + for (const userDid of userDids) { + try { + const chatResponse = await fetch(`${atprotoApi}/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); + } + } catch (err) { + // Skip failed users + continue; + } + } // Filter out old records with invalid AI profile data (temporary fix for migration) - const validRecords = chatRecords.filter(record => { + const validRecords = allChatRecords.filter(record => { if (record.value.answer) { // This is an AI answer - check if it has valid AI profile return record.value.author?.handle && @@ -266,16 +252,13 @@ function App() { return true; // Keep all questions }); - console.log(`Filtered ${chatRecords.length} records to ${validRecords.length} valid records`); - - // Sort by creation time and group question-answer pairs + // Sort by creation time const sortedRecords = validRecords.sort((a, b) => - new Date(a.value.createdAt).getTime() - new Date(b.value.createdAt).getTime() + new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime() ); setAiChatHistory(sortedRecords); } catch (err) { - console.error('Failed to load AI chat history:', err); setAiChatHistory([]); } }; @@ -284,58 +267,64 @@ function App() { const loadAIGeneratedContent = async () => { try { const adminDid = appConfig.adminDid; - const bskyApi = appConfig.bskyPublicApi || 'https://public.api.bsky.app'; + const atprotoApi = appConfig.atprotoApi || 'https://bsky.social'; const collections = getCollectionNames(appConfig.collections.base); // Load lang:en records - const langResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`); + const langResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`); if (langResponse.ok) { const langData = await langResponse.json(); const langRecords = langData.records || []; - // Filter by current page URL if on post page + // Filter by current page path if on post page const filteredLangRecords = appConfig.rkey - ? langRecords.filter(record => record.value.url === window.location.href) + ? langRecords.filter(record => { + // Compare path only, not full URL to support localhost vs production + const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname : + record.value.url ? new URL(record.value.url).pathname : ''; + return recordPath === window.location.pathname; + }) : langRecords.slice(0, 3); // Top page: latest 3 setLangEnRecords(filteredLangRecords); } // Load AI comment records - const commentResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`); + const commentResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`); if (commentResponse.ok) { const commentData = await commentResponse.json(); const commentRecords = commentData.records || []; - // Filter by current page URL if on post page + // Filter by current page path if on post page const filteredCommentRecords = appConfig.rkey - ? commentRecords.filter(record => record.value.url === window.location.href) + ? commentRecords.filter(record => { + // Compare path only, not full URL to support localhost vs production + const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname : + record.value.url ? new URL(record.value.url).pathname : ''; + return recordPath === window.location.pathname; + }) : commentRecords.slice(0, 3); // Top page: latest 3 setAiCommentRecords(filteredCommentRecords); } } catch (err) { - console.error('Failed to load AI generated content:', err); + // Ignore errors } }; const loadUserComments = async (did: string) => { try { - console.log('Loading comments for DID:', did); const agent = atprotoOAuthService.getAgent(); if (!agent) { - console.log('No agent available'); return; } // Get comments from current user const response = await agent.api.com.atproto.repo.listRecords({ repo: did, - collection: appConfig.collections.comment, + collection: getCollectionNames(appConfig.collections.base).comment, limit: 100, }); - - console.log('User comments loaded:', response.data); const userComments = response.data.records || []; // Enhance comments with profile information if missing @@ -356,7 +345,7 @@ function App() { } }; } catch (err) { - console.warn('Failed to enhance comment with profile:', err); + // Ignore enhancement errors return record; } } @@ -366,7 +355,7 @@ function App() { setComments(enhancedComments); } catch (err) { - console.error('Failed to load comments:', err); + // Ignore load errors setComments([]); } }; @@ -376,20 +365,20 @@ function App() { try { // 管理者のユーザーリストを取得 const adminDid = appConfig.adminDid; - console.log('Fetching user list from admin DID:', adminDid); - const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`); + // Fetching user list from admin DID + const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`); if (!response.ok) { - console.warn('Failed to fetch user list from admin, using default users. Status:', response.status); + // Failed to fetch user list from admin, using default users return getDefaultUsers(); } const data = await response.json(); const userRecords = data.records || []; - console.log('User records found:', userRecords.length); + // User records found if (userRecords.length === 0) { - console.log('No user records found, using default users'); + // No user records found, using default users return getDefaultUsers(); } @@ -401,13 +390,13 @@ function App() { const resolvedUsers = await Promise.all( record.value.users.map(async (user) => { if (user.did && user.did.includes('-placeholder')) { - console.log(`Resolving placeholder DID for ${user.handle}`); + // Resolving placeholder DID try { const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`); if (profileResponse.ok) { const profileData = await profileResponse.json(); if (profileData.did) { - console.log(`Resolved ${user.handle}: ${user.did} -> ${profileData.did}`); + // Resolved DID return { ...user, did: profileData.did @@ -415,7 +404,7 @@ function App() { } } } catch (err) { - console.warn(`Failed to resolve DID for ${user.handle}:`, err); + // Failed to resolve DID } } return user; @@ -425,10 +414,10 @@ function App() { } } - console.log('Loaded and resolved users from admin records:', allUsers); + // Loaded and resolved users from admin records return allUsers; } catch (err) { - console.warn('Failed to load users from records, using defaults:', err); + // Failed to load users from records, using defaults return getDefaultUsers(); } }; @@ -436,12 +425,12 @@ function App() { // ユーザーリスト一覧を読み込み const loadUserListRecords = async () => { try { - console.log('Loading user list records...'); + // Loading user list records const adminDid = appConfig.adminDid; - const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`); + const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`); if (!response.ok) { - console.warn('Failed to fetch user list records'); + // Failed to fetch user list records setUserListRecords([]); return; } @@ -454,10 +443,10 @@ function App() { new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime() ); - console.log(`Loaded ${sortedRecords.length} user list records`); + // Loaded user list records setUserListRecords(sortedRecords); } catch (err) { - console.error('Failed to load user list records:', err); + // Failed to load user list records setUserListRecords([]); } }; @@ -477,39 +466,33 @@ function App() { }); } - console.log('Default users list (including current user):', defaultUsers); + // Default users list (including current user) return defaultUsers; }; // 新しい関数: 全ユーザーからコメントを収集 const loadAllComments = async (pageUrl?: string) => { try { - console.log('Loading comments from all users...'); - console.log('Page URL filter:', pageUrl); // ユーザーリストを動的に取得 const knownUsers = await loadUsersFromRecord(); - console.log('Known users for comment fetching:', knownUsers); const allComments = []; // 各ユーザーからコメントを収集 for (const user of knownUsers) { try { - console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`); // Public API使用(認証不要) const collections = getCollectionNames(appConfig.collections.base); const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`); if (!response.ok) { - console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`); continue; } const data = await response.json(); const userRecords = data.records || []; - console.log(`Found ${userRecords.length} comment records from ${user.handle}`); // Flatten comments from new array format const userComments = []; @@ -529,18 +512,24 @@ function App() { } } - console.log(`Flattened to ${userComments.length} individual comments from ${user.handle}`); - // ページURLでフィルタリング(指定された場合) + // ページpathでフィルタリング(指定された場合) const filteredComments = pageUrl - ? userComments.filter(record => record.value.url === pageUrl) + ? userComments.filter(record => { + try { + // Compare path only, not full URL to support localhost vs production + const recordPath = record.value.url ? new URL(record.value.url).pathname : ''; + const currentPath = new URL(pageUrl).pathname; + return recordPath === currentPath; + } catch (err) { + // Fallback to exact match if URL parsing fails + return record.value.url === pageUrl; + } + }) : userComments; - console.log(`After URL filtering (${pageUrl}): ${filteredComments.length} comments from ${user.handle}`); - console.log('All comments from this user:', userComments.map(r => ({ url: r.value.url, text: r.value.text }))); allComments.push(...filteredComments); } catch (err) { - console.warn(`Failed to load comments from ${user.handle}:`, err); } } @@ -572,21 +561,17 @@ function App() { }; } } catch (err) { - console.warn('Failed to enhance comment with profile:', err); + // Ignore enhancement errors } } return record; }) ); - console.log(`Loaded ${enhancedComments.length} comments from all users`); // デバッグ情報を追加 - console.log('Final enhanced comments:', enhancedComments); - console.log('Known users used:', knownUsers); setComments(enhancedComments); - console.log('Comments state updated with', enhancedComments.length, 'comments'); // キャッシュに保存(5分間有効) if (pageUrl) { @@ -598,7 +583,6 @@ function App() { localStorage.setItem(cacheKey, JSON.stringify(cacheData)); } } catch (err) { - console.error('Failed to load all comments:', err); setComments([]); } }; @@ -640,7 +624,7 @@ function App() { try { const existingResponse = await agent.api.com.atproto.repo.getRecord({ repo: user.did, - collection: appConfig.collections.comment, + collection: getCollectionNames(appConfig.collections.base).comment, rkey: rkey, }); @@ -659,7 +643,6 @@ function App() { } } catch (err) { // Record doesn't exist yet, that's fine - console.log('No existing record found, creating new one'); } // Add new comment to the array @@ -667,7 +650,7 @@ function App() { // Create the record with comments array const record = { - $type: appConfig.collections.comment, + $type: getCollectionNames(appConfig.collections.base).comment, comments: existingComments, url: window.location.href, createdAt: now.toISOString(), // Latest update time @@ -676,18 +659,16 @@ function App() { // Post to ATProto with rkey const response = await agent.api.com.atproto.repo.putRecord({ repo: user.did, - collection: appConfig.collections.comment, + collection: getCollectionNames(appConfig.collections.base).comment, rkey: rkey, record: record, }); - console.log('Comment posted:', response); // Clear form and reload all comments setCommentText(''); await loadAllComments(window.location.href); } catch (err: any) { - console.error('Failed to post comment:', err); setError('コメントの投稿に失敗しました: ' + err.message); } finally { setIsPosting(false); @@ -714,22 +695,19 @@ function App() { const uriParts = uri.split('/'); const rkey = uriParts[uriParts.length - 1]; - console.log('Deleting comment with rkey:', rkey); // Delete the record await agent.api.com.atproto.repo.deleteRecord({ repo: user.did, - collection: appConfig.collections.comment, + collection: getCollectionNames(appConfig.collections.base).comment, rkey: rkey, }); - console.log('Comment deleted successfully'); // Reload all comments to reflect the deletion await loadAllComments(window.location.href); } catch (err: any) { - console.error('Failed to delete comment:', err); alert('コメントの削除に失敗しました: ' + err.message); } }; @@ -787,11 +765,9 @@ function App() { const profileData = await profileResponse.json(); if (profileData.did) { resolvedDid = profileData.did; - console.log(`Resolved ${handle} -> ${resolvedDid}`); } } } catch (err) { - console.warn(`Failed to resolve DID for ${handle}:`, err); } return { @@ -806,7 +782,7 @@ function App() { const rkey = now.toISOString().replace(/[:.]/g, '-'); const record = { - $type: appConfig.collections.user, + $type: getCollectionNames(appConfig.collections.base).user, users: users, createdAt: now.toISOString(), updatedBy: { @@ -818,19 +794,17 @@ function App() { // Post to ATProto with rkey const response = await agent.api.com.atproto.repo.putRecord({ repo: user.did, - collection: appConfig.collections.user, + collection: getCollectionNames(appConfig.collections.base).user, rkey: rkey, record: record, }); - console.log('User list posted:', response); // Clear form and reload user list records setUserListInput(''); loadUserListRecords(); alert('ユーザーリストが更新されました'); } catch (err: any) { - console.error('Failed to post user list:', err); setError('ユーザーリストの投稿に失敗しました: ' + err.message); } finally { setIsPostingUserList(false); @@ -858,21 +832,18 @@ function App() { const uriParts = uri.split('/'); const rkey = uriParts[uriParts.length - 1]; - console.log('Deleting user list with rkey:', rkey); // Delete the record await agent.api.com.atproto.repo.deleteRecord({ repo: user.did, - collection: appConfig.collections.user, + collection: getCollectionNames(appConfig.collections.base).user, rkey: rkey, }); - console.log('User list deleted successfully'); loadUserListRecords(); alert('ユーザーリストが削除されました'); } catch (err: any) { - console.error('Failed to delete user list:', err); alert('ユーザーリストの削除に失敗しました: ' + err.message); } }; @@ -895,7 +866,6 @@ function App() { try { await atprotoOAuthService.initiateOAuthFlow(handleInput); } catch (err) { - console.error('OAuth failed:', err); alert('認証の開始に失敗しました。再度お試しください。'); } }; @@ -918,7 +888,13 @@ function App() { } // Extract rkey from comment URI: at://did:plc:xxx/collection/rkey - const uriParts = record.uri.split('/'); + // 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 @@ -926,12 +902,130 @@ function App() { }; // OAuth callback is now handled by React Router in main.tsx - console.log('=== APP.TSX URL CHECK ==='); - console.log('Full URL:', window.location.href); - console.log('Pathname:', window.location.pathname); - console.log('Search params:', window.location.search); - console.log('=== END URL CHECK ==='); + // 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 || ''); + const authorInfo = isNewFormat ? value.author : null; + const postInfo = isNewFormat ? value.post : null; + const contentType = value.type || 'unknown'; + const createdAt = value.createdAt || value.generated_at || ''; + + return ( +
+
+ AI Avatar { + // For old format, try to fetch from ai_did + if (img && !isNewFormat && value.ai_did) { + fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(value.ai_did)}`) + .then(res => res.json()) + .then(data => { + if (data.avatar && img) { + img.src = data.avatar; + } + }) + .catch(err => { + // Keep placeholder on error + }); + } + }} + /> +
+ + {authorInfo?.displayName || 'AI'} + + + @{authorInfo?.handle || 'ai'} + + {getTypeLabel(value.$type, contentType)} +
+ + {new Date(createdAt).toLocaleString()} + +
+ +
+
+ + {/* Post information for new format */} + {postInfo && ( +
+

{postInfo.title}

+ {postInfo.tags && ( +
+ {postInfo.tags.map((tag: string) => ( + #{tag} + ))} +
+ )} +
+ )} + + {/* Legacy post information for old format */} + {!postInfo && (value.post_title || value.post_url) && ( +
+

{value.post_title || 'Unknown'}

+
+ )} + +
+ {contentText} +
+ +
+ {(postInfo?.url || value.post_url) && ( + + + {postInfo?.url || value.post_url} + + + )} +
+ + {/* JSON Display */} + {showJsonFor === record.uri && ( +
+
JSON Record:
+
+              {JSON.stringify(record, null, 2)}
+            
+
+ )} +
+ ); + }; + + 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 (
@@ -1081,14 +1175,12 @@ function App() { > Comments ({comments.filter(shouldShowComment).length}) - {user && ( - - )} +