(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,28 +203,73 @@ 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 users in admin's user list
+ 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,
+
+ 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);
+ }
+ });
+ }
});
-
- console.log('AI chat history loaded:', response.data);
- const chatRecords = response.data.records || [];
+
+ // Always include admin DID to check admin's own chats
+ allUserDids.push(adminDid);
+
+ const userDids = [...new Set(allUserDids)];
+
+ // Load chat records from all registered users (including admin)
+ 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) {
+ 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 recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname : '';
+ return recordPath === window.location.pathname;
+ });
+ } 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 = chatRecords.filter(record => {
- if (record.value.answer) {
+ 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' &&
@@ -266,16 +278,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 +293,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 +371,7 @@ function App() {
}
};
} catch (err) {
- console.warn('Failed to enhance comment with profile:', err);
+ // Ignore enhancement errors
return record;
}
}
@@ -366,7 +381,7 @@ function App() {
setComments(enhancedComments);
} catch (err) {
- console.error('Failed to load comments:', err);
+ // Ignore load errors
setComments([]);
}
};
@@ -376,20 +391,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 +416,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 +430,7 @@ function App() {
}
}
} catch (err) {
- console.warn(`Failed to resolve DID for ${user.handle}:`, err);
+ // Failed to resolve DID
}
}
return user;
@@ -425,10 +440,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 +451,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 +469,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 +492,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 +538,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 +587,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 +609,6 @@ function App() {
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
}
} catch (err) {
- console.error('Failed to load all comments:', err);
setComments([]);
}
};
@@ -640,7 +650,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 +669,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 +676,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 +685,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 +721,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 +791,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 +808,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 +820,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 +858,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 +892,6 @@ function App() {
try {
await atprotoOAuthService.initiateOAuthFlow(handleInput);
} catch (err) {
- console.error('OAuth failed:', err);
alert('認証の開始に失敗しました。再度お試しください。');
}
};
@@ -918,7 +914,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 +928,113 @@ 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 (
+
+
+

{
+ // 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'}
+
+
+
+ {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 (
@@ -1081,14 +1184,12 @@ function App() {
>
Comments ({comments.filter(shouldShowComment).length})
- {user && (
-
- )}
+
-
- {record.value.text}
-
+
)}
+
+
+ {record.value.text?.split('\n').map((line: string, index: number) => (
+
+ {line}
+ {index < record.value.text.split('\n').length - 1 &&
}
+
+ ))}
+
))
)}
@@ -1196,7 +1303,7 @@ function App() {
)}
{/* AI Chat History List */}
- {activeTab === 'ai-chat' && user && (
+ {activeTab === 'ai-chat' && (
AI Chat History
@@ -1204,43 +1311,49 @@ function App() {
{aiChatHistory.length === 0 ? (
No AI conversations yet. Start chatting with Ask AI!
) : (
- aiChatHistory.map((record, index) => (
-
-
-

{
- // Fetch fresh avatar from API when component mounts
- if (img && record.value.author?.did) {
- fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.did)}`)
- .then(res => res.json())
- .then(data => {
- if (data.avatar && img) {
- img.src = data.avatar;
- }
- })
- .catch(err => {
- console.warn('Failed to fetch fresh avatar:', err);
- // Keep placeholder on error
- });
- }
- }}
- />
-
+ aiChatHistory.map((record, index) => {
+ // For AI responses, use AI DID; for user questions, use the actual author
+ const isAiResponse = record.value.type === 'answer';
+ const displayDid = isAiResponse ? appConfig.aiDid : record.value.author?.did;
+ const displayHandle = isAiResponse ? 'ai.syui' : record.value.author?.handle;
+ const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle);
+
+ return (
+
+
+

{
+ // Fetch fresh avatar from API when component mounts
+ if (img && displayDid) {
+ fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(displayDid)}`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.avatar && img) {
+ img.src = data.avatar;
+ }
+ })
+ .catch(err => {
+ // Keep placeholder on error
+ });
+ }
+ }}
+ />
+
{new Date(record.value.createdAt).toLocaleString()}
@@ -1253,16 +1366,14 @@ function App() {
{showJsonFor === record.uri ? 'Hide' : 'JSON'}
-
- {record.value.question || record.value.answer}
-
+
@@ -1275,8 +1386,18 @@ function App() {
)}
+
+
+ {record.value.text?.split('\n').map((line: string, index: number) => (
+
+ {line}
+ {index < record.value.text.split('\n').length - 1 &&
}
+
+ ))}
+
- ))
+ );
+ })
)}
)}
@@ -1287,76 +1408,88 @@ function App() {
{langEnRecords.length === 0 ? (
No English translations yet
) : (
- langEnRecords.map((record, index) => (
-
-
-

-
-
- {record.value.author?.displayName || 'AI Translator'}
-
-
- @{record.value.author?.handle || 'ai'}
-
-
-
- {new Date(record.value.createdAt).toLocaleString()}
-
-
-
-
Type: {record.value.type || 'en'}
-
{record.value.body}
-
-
-
- ))
+ 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) => (
-
-
+
+

{
+ // Fetch AI avatar
+ if (img && appConfig.aiDid) {
+ fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(appConfig.aiDid)}`)
+ .then(res => res.json())
+ .then(data => {
+ if (data.avatar && img) {
+ img.src = data.avatar;
+ }
+ })
+ .catch(err => {
+ // Keep placeholder on error
+ });
+ }
+ }}
/>
- {record.value.author?.displayName || 'AI Commenter'}
+ AI
- @{record.value.author?.handle || 'ai'}
+ @ai
- {new Date(record.value.createdAt).toLocaleString()}
+ {new Date(record.value.createdAt || record.value.generated_at).toLocaleString()}
+
+
+
-
-
Type: {record.value.type || 'comment'}
-
{record.value.body}
-
+
+
+ {/* JSON Display */}
+ {showJsonFor === record.uri && (
+
+
JSON Record:
+
+ {JSON.stringify(record, null, 2)}
+
+
+ )}
+
+
+ {(record.value.text || record.value.comment)?.split('\n').map((line: string, index: number) => (
+
+ {line}
+ {index < (record.value.text || record.value.comment)?.split('\n').length - 1 &&
}
+
+ ))}
+
))
)}
diff --git a/oauth/src/components/AIChat.tsx b/oauth/src/components/AIChat.tsx
index ffceb0b..8ebce70 100644
--- a/oauth/src/components/AIChat.tsx
+++ b/oauth/src/components/AIChat.tsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { User } from '../services/auth';
import { atprotoOAuthService } from '../services/atproto-oauth';
-import { appConfig } from '../config/app';
+import { appConfig, getCollectionNames } from '../config/app';
interface AIChatProps {
user: User | null;
@@ -14,26 +14,22 @@ export const AIChat: React.FC
= ({ user, isEnabled }) => {
const [isProcessing, setIsProcessing] = useState(false);
const [aiProfile, setAiProfile] = useState(null);
- // Get AI settings from environment variables
+ // Get AI settings from appConfig (unified configuration)
const aiConfig = {
- enabled: import.meta.env.VITE_AI_ENABLED === 'true',
- askAi: import.meta.env.VITE_AI_ASK_AI === 'true',
- provider: import.meta.env.VITE_AI_PROVIDER || 'ollama',
- model: import.meta.env.VITE_AI_MODEL || 'gemma3:4b',
- host: import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai',
- systemPrompt: import.meta.env.VITE_AI_SYSTEM_PROMPT || 'You are a helpful AI assistant trained on this blog\'s content.',
- aiDid: import.meta.env.VITE_AI_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn',
- bskyPublicApi: import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app',
+ 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 () => {
- console.log('=== AI PROFILE FETCH START ===');
- console.log('AI DID:', aiConfig.aiDid);
-
if (!aiConfig.aiDid) {
- console.log('No AI DID configured');
return;
}
@@ -41,9 +37,7 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
// Try with agent first
const agent = atprotoOAuthService.getAgent();
if (agent) {
- console.log('Fetching AI profile with agent for DID:', aiConfig.aiDid);
const profile = await agent.getProfile({ actor: aiConfig.aiDid });
- console.log('AI profile fetched successfully:', profile.data);
const profileData = {
did: aiConfig.aiDid,
handle: profile.data.handle,
@@ -51,21 +45,17 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
avatar: profile.data.avatar,
description: profile.data.description
};
- console.log('Setting aiProfile to:', profileData);
setAiProfile(profileData);
// Dispatch event to update Ask AI button
window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profileData }));
- console.log('=== AI PROFILE FETCH SUCCESS (AGENT) ===');
return;
}
// Fallback to public API
- console.log('No agent available, trying public API for AI profile');
const response = await fetch(`${aiConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(aiConfig.aiDid)}`);
if (response.ok) {
const profileData = await response.json();
- console.log('AI profile fetched via public API:', profileData);
const profile = {
did: aiConfig.aiDid,
handle: profileData.handle,
@@ -73,21 +63,15 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
avatar: profileData.avatar,
description: profileData.description
};
- console.log('Setting aiProfile to:', profile);
setAiProfile(profile);
// Dispatch event to update Ask AI button
window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profile }));
- console.log('=== AI PROFILE FETCH SUCCESS (PUBLIC API) ===');
return;
- } else {
- console.error('Public API failed with status:', response.status);
}
} catch (error) {
- console.error('Failed to fetch AI profile:', error);
setAiProfile(null);
}
- console.log('=== AI PROFILE FETCH FAILED ===');
};
fetchAIProfile();
@@ -100,9 +84,6 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
const handleAIQuestion = async (event: any) => {
if (!user || !event.detail || !event.detail.question || isProcessing || !aiProfile) return;
- console.log('AIChat received question:', event.detail.question);
- console.log('Current aiProfile state:', aiProfile);
-
setIsProcessing(true);
try {
await postQuestionAndGenerateResponse(event.detail.question);
@@ -114,7 +95,6 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
// Add listener with a small delay to ensure it's ready
setTimeout(() => {
window.addEventListener('postAIQuestion', handleAIQuestion);
- console.log('AIChat event listener registered');
// Notify that AI is ready
window.dispatchEvent(new CustomEvent('aiChatReady'));
@@ -134,40 +114,50 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
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: appConfig.collections.chat,
- question: question,
- url: window.location.href,
- createdAt: now.toISOString(),
+ $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,
},
- context: {
- page_title: document.title,
- page_url: window.location.href,
- },
+ createdAt: now.toISOString(),
};
await agent.api.com.atproto.repo.putRecord({
repo: user.did,
- collection: appConfig.collections.chat,
+ collection: collections.chat,
rkey: rkey,
record: questionRecord,
});
- console.log('Question posted to ATProto');
-
// 2. Get chat history
const chatRecords = await agent.api.com.atproto.repo.listRecords({
repo: user.did,
- collection: appConfig.collections.chat,
+ collection: collections.chat,
limit: 10,
});
@@ -175,10 +165,10 @@ export const AIChat: React.FC = ({ user, isEnabled }) => {
if (chatRecords.data.records) {
chatHistoryText = chatRecords.data.records
.map((r: any) => {
- if (r.value.question) {
- return `User: ${r.value.question}`;
- } else if (r.value.answer) {
- return `AI: ${r.value.answer}`;
+ if (r.value.type === 'question') {
+ return `User: ${r.value.text}`;
+ } else if (r.value.type === 'answer') {
+ return `AI: ${r.value.text}`;
}
return '';
})
@@ -235,37 +225,38 @@ Answer:`;
// 5. Save AI response in background
const answerRkey = now.toISOString().replace(/[:.]/g, '-') + '-answer';
- console.log('=== SAVING AI ANSWER ===');
- console.log('Current aiProfile:', aiProfile);
-
const answerRecord = {
- $type: appConfig.collections.chat,
- answer: aiAnswer,
- question_rkey: rkey,
- url: window.location.href,
- createdAt: now.toISOString(),
+ $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(),
};
-
- console.log('Answer record to save:', answerRecord);
// Save to ATProto asynchronously (don't wait for it)
agent.api.com.atproto.repo.putRecord({
repo: user.did,
- collection: appConfig.collections.chat,
+ collection: collections.chat,
rkey: answerRkey,
record: answerRecord,
}).catch(err => {
- console.error('Failed to save AI response to ATProto:', err);
+ // Silent fail for AI response saving
});
} catch (error) {
- console.error('Failed to generate AI response:', error);
window.dispatchEvent(new CustomEvent('aiResponseError', {
detail: { error: 'AI応答の生成に失敗しました' }
}));
diff --git a/oauth/src/config/app.ts b/oauth/src/config/app.ts
index 5779f30..7c9dc4b 100644
--- a/oauth/src/config/app.ts
+++ b/oauth/src/config/app.ts
@@ -1,6 +1,7 @@
// Application configuration
export interface AppConfig {
adminDid: string;
+ aiDid: string;
collections: {
base: string; // Base collection like "ai.syui.log"
};
@@ -11,18 +12,27 @@ export interface AppConfig {
aiProvider: string;
aiModel: string;
aiHost: string;
+ aiSystemPrompt: string;
bskyPublicApi: string;
+ atprotoApi: string;
}
// Collection name builders (similar to Rust implementation)
export function getCollectionNames(base: string) {
- return {
+ if (!base) {
+ // Fallback to default
+ base = 'ai.syui.log';
+ }
+
+ const collections = {
comment: base,
user: `${base}.user`,
chat: `${base}.chat`,
chatLang: `${base}.chat.lang`,
chatComment: `${base}.chat.comment`,
};
+
+ return collections;
}
// Generate collection names from host
@@ -43,9 +53,9 @@ function generateBaseCollectionFromHost(host: string): string {
// Reverse the parts for collection naming
// log.syui.ai -> ai.syui.log
const reversedParts = parts.reverse();
- return reversedParts.join('.');
+ const result = reversedParts.join('.');
+ return result;
} catch (error) {
- console.warn('Failed to generate collection base from host:', host, error);
// Fallback to default
return 'ai.syui.log';
}
@@ -63,11 +73,19 @@ function extractRkeyFromUrl(): string | undefined {
export function getAppConfig(): AppConfig {
const host = import.meta.env.VITE_APP_HOST || 'https://log.syui.ai';
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
+ const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:4hqjfn7m6n5hno3doamuhgef';
// Priority: Environment variables > Auto-generated from host
const autoGeneratedBase = generateBaseCollectionFromHost(host);
+ let baseCollection = import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase;
+
+ // Ensure base collection is never undefined
+ if (!baseCollection) {
+ baseCollection = 'ai.syui.log';
+ }
+
const collections = {
- base: import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase,
+ base: baseCollection,
};
const rkey = extractRkeyFromUrl();
@@ -78,19 +96,14 @@ export function getAppConfig(): AppConfig {
const aiProvider = import.meta.env.VITE_AI_PROVIDER || 'ollama';
const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b';
const aiHost = import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai';
+ const aiSystemPrompt = import.meta.env.VITE_AI_SYSTEM_PROMPT || 'You are a helpful AI assistant trained on this blog\'s content.';
const bskyPublicApi = import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app';
+ const atprotoApi = import.meta.env.VITE_ATPROTO_API || 'https://bsky.social';
- console.log('App configuration:', {
- host,
- adminDid,
- collections,
- rkey: rkey || 'none (not on post page)',
- ai: { enabled: aiEnabled, askAi: aiAskAi, provider: aiProvider, model: aiModel, host: aiHost },
- bskyPublicApi
- });
return {
adminDid,
+ aiDid,
collections,
host,
rkey,
@@ -99,7 +112,9 @@ export function getAppConfig(): AppConfig {
aiProvider,
aiModel,
aiHost,
- bskyPublicApi
+ aiSystemPrompt,
+ bskyPublicApi,
+ atprotoApi
};
}
diff --git a/oauth/src/services/api.ts b/oauth/src/services/api.ts
index 778a25a..3c2d48d 100644
--- a/oauth/src/services/api.ts
+++ b/oauth/src/services/api.ts
@@ -73,7 +73,6 @@ export const aiCardApi = {
});
return response.data.data;
} catch (error) {
- console.warn('ai.gpt AI分析機能が利用できません:', error);
throw new Error('AI分析機能を利用するにはai.gptサーバーが必要です');
}
},
@@ -86,7 +85,6 @@ export const aiCardApi = {
const response = await aiGptApi.get('/card_get_gacha_stats');
return response.data.data;
} catch (error) {
- console.warn('ai.gpt AI統計機能が利用できません:', error);
throw new Error('AI統計機能を利用するにはai.gptサーバーが必要です');
}
},
diff --git a/oauth/src/services/atproto-oauth.ts b/oauth/src/services/atproto-oauth.ts
index e4851bf..f5b71ab 100644
--- a/oauth/src/services/atproto-oauth.ts
+++ b/oauth/src/services/atproto-oauth.ts
@@ -31,11 +31,11 @@ class AtprotoOAuthService {
private async _doInitialize(): Promise {
try {
- console.log('=== INITIALIZING ATPROTO OAUTH CLIENT ===');
+
// Generate client ID based on current origin
const clientId = this.getClientId();
- console.log('Client ID:', clientId);
+
// Support multiple PDS hosts for OAuth
this.oauthClient = await BrowserOAuthClient.load({
@@ -43,39 +43,33 @@ class AtprotoOAuthService {
handleResolver: 'https://bsky.social', // Default resolver
});
- console.log('BrowserOAuthClient initialized successfully with multi-PDS support');
+
// Try to restore existing session
const result = await this.oauthClient.init();
if (result?.session) {
- console.log('Existing session restored:', {
- did: result.session.did,
- handle: result.session.handle || 'unknown',
- hasAccessJwt: !!result.session.accessJwt,
- hasRefreshJwt: !!result.session.refreshJwt
- });
// Create Agent instance with proper configuration
- console.log('Creating Agent with session:', result.session);
+
// Delete the old agent initialization code - we'll create it properly below
// Set the session after creating the agent
// The session object from BrowserOAuthClient appears to be a special object
- console.log('Full session object:', result.session);
- console.log('Session type:', typeof result.session);
- console.log('Session constructor:', result.session?.constructor?.name);
+
+
+
// Try to iterate over the session object
if (result.session) {
- console.log('Session properties:');
+
for (const key in result.session) {
- console.log(` ${key}:`, result.session[key]);
+
}
// Check if session has methods
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(result.session));
- console.log('Session methods:', methods);
+
}
// BrowserOAuthClient might return a Session object that needs to be used with the agent
@@ -83,36 +77,36 @@ class AtprotoOAuthService {
if (result.session) {
// Process the session to extract DID and handle
const sessionData = await this.processSession(result.session);
- console.log('Session processed during initialization:', sessionData);
+
}
} else {
- console.log('No existing session found');
+
}
} catch (error) {
- console.error('Failed to initialize OAuth client:', error);
+
this.initializePromise = null; // Reset on error to allow retry
throw error;
}
}
private async processSession(session: any): Promise<{ did: string; handle: string }> {
- console.log('Processing session:', session);
+
// Log full session structure
- console.log('Session structure:');
- console.log('- sub:', session.sub);
- console.log('- did:', session.did);
- console.log('- handle:', session.handle);
- console.log('- iss:', session.iss);
- console.log('- aud:', session.aud);
+
+
+
+
+
+
// Check if agent has properties we can access
if (session.agent) {
- console.log('- agent:', session.agent);
- console.log('- agent.did:', session.agent?.did);
- console.log('- agent.handle:', session.agent?.handle);
+
+
+
}
const did = session.sub || session.did;
@@ -121,18 +115,18 @@ class AtprotoOAuthService {
// Create Agent directly with session (per official docs)
try {
this.agent = new Agent(session);
- console.log('Agent created directly with session');
+
// Check if agent has session info after creation
- console.log('Agent after creation:');
- console.log('- agent.did:', this.agent.did);
- console.log('- agent.session:', this.agent.session);
+
+
+
if (this.agent.session) {
- console.log('- agent.session.did:', this.agent.session.did);
- console.log('- agent.session.handle:', this.agent.session.handle);
+
+
}
} catch (err) {
- console.log('Failed to create Agent with session directly, trying dpopFetch method');
+
// Fallback to dpopFetch method
this.agent = new Agent({
service: session.server?.serviceEndpoint || 'https://bsky.social',
@@ -145,7 +139,7 @@ class AtprotoOAuthService {
// If handle is missing, try multiple methods to resolve it
if (!handle || handle === 'unknown') {
- console.log('Handle not in session, attempting to resolve...');
+
// Method 1: Try using the agent to get profile
try {
@@ -154,11 +148,11 @@ class AtprotoOAuthService {
if (profile.data.handle) {
handle = profile.data.handle;
(this as any)._sessionInfo.handle = handle;
- console.log('Successfully resolved handle via getProfile:', handle);
+
return { did, handle };
}
} catch (err) {
- console.error('getProfile failed:', err);
+
}
// Method 2: Try using describeRepo
@@ -169,18 +163,20 @@ class AtprotoOAuthService {
if (repoDesc.data.handle) {
handle = repoDesc.data.handle;
(this as any)._sessionInfo.handle = handle;
- console.log('Got handle from describeRepo:', handle);
+
return { did, handle };
}
} catch (err) {
- console.error('describeRepo failed:', err);
+
}
- // Method 3: Hardcoded fallback for known DIDs
- if (did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
- handle = 'syui.ai';
+ // Method 3: Fallback for admin DID
+ const adminDid = import.meta.env.VITE_ADMIN_DID;
+ if (did === adminDid) {
+ const appHost = import.meta.env.VITE_APP_HOST || 'https://syui.ai';
+ handle = new URL(appHost).hostname;
(this as any)._sessionInfo.handle = handle;
- console.log('Using hardcoded handle for known DID');
+
}
}
@@ -191,7 +187,7 @@ class AtprotoOAuthService {
// Use environment variable if available
const envClientId = import.meta.env.VITE_OAUTH_CLIENT_ID;
if (envClientId) {
- console.log('Using client ID from environment:', envClientId);
+
return envClientId;
}
@@ -200,7 +196,7 @@ class AtprotoOAuthService {
// For localhost development, use undefined for loopback client
// The BrowserOAuthClient will handle this automatically
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
- console.log('Using loopback client for localhost development');
+
return undefined as any; // Loopback client
}
@@ -209,7 +205,7 @@ class AtprotoOAuthService {
}
private detectPDSFromHandle(handle: string): string {
- console.log('Detecting PDS for handle:', handle);
+
// Supported PDS hosts and their corresponding handles
const pdsMapping = {
@@ -220,22 +216,22 @@ class AtprotoOAuthService {
// Check if handle ends with known PDS domains
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
if (handle.endsWith(`.${domain}`)) {
- console.log(`Handle ${handle} mapped to PDS: ${pdsUrl}`);
+
return pdsUrl;
}
}
// Default to bsky.social
- console.log(`Handle ${handle} using default PDS: https://bsky.social`);
+
return 'https://bsky.social';
}
async initiateOAuthFlow(handle?: string): Promise {
try {
- console.log('=== INITIATING OAUTH FLOW ===');
+
if (!this.oauthClient) {
- console.log('OAuth client not initialized, initializing now...');
+
await this.initialize();
}
@@ -251,15 +247,15 @@ class AtprotoOAuthService {
}
}
- console.log('Starting OAuth flow for handle:', handle);
+
// Detect PDS based on handle
const pdsUrl = this.detectPDSFromHandle(handle);
- console.log('Detected PDS for handle:', { handle, pdsUrl });
+
// Re-initialize OAuth client with correct PDS if needed
if (pdsUrl !== 'https://bsky.social') {
- console.log('Re-initializing OAuth client for custom PDS:', pdsUrl);
+
this.oauthClient = await BrowserOAuthClient.load({
clientId: this.getClientId(),
handleResolver: pdsUrl,
@@ -267,20 +263,14 @@ class AtprotoOAuthService {
}
// Start OAuth authorization flow
- console.log('Calling oauthClient.authorize with handle:', handle);
+
try {
const authUrl = await this.oauthClient.authorize(handle, {
scope: 'atproto transition:generic',
});
- console.log('Authorization URL generated:', authUrl.toString());
- console.log('URL breakdown:', {
- protocol: authUrl.protocol,
- hostname: authUrl.hostname,
- pathname: authUrl.pathname,
- search: authUrl.search
- });
+
// Store some debug info before redirect
sessionStorage.setItem('oauth_debug_pre_redirect', JSON.stringify({
@@ -291,35 +281,30 @@ class AtprotoOAuthService {
}));
// Redirect to authorization server
- console.log('About to redirect to:', authUrl.toString());
+
window.location.href = authUrl.toString();
} catch (authorizeError) {
- console.error('oauthClient.authorize failed:', authorizeError);
- console.error('Error details:', {
- name: authorizeError.name,
- message: authorizeError.message,
- stack: authorizeError.stack
- });
+
throw authorizeError;
}
} catch (error) {
- console.error('Failed to initiate OAuth flow:', error);
+
throw new Error(`OAuth認証の開始に失敗しました: ${error}`);
}
}
async handleOAuthCallback(): Promise<{ did: string; handle: string } | null> {
try {
- console.log('=== HANDLING OAUTH CALLBACK ===');
- console.log('Current URL:', window.location.href);
- console.log('URL hash:', window.location.hash);
- console.log('URL search:', window.location.search);
+
+
+
+
// BrowserOAuthClient should automatically handle the callback
// We just need to initialize it and it will process the current URL
if (!this.oauthClient) {
- console.log('OAuth client not initialized, initializing now...');
+
await this.initialize();
}
@@ -327,11 +312,11 @@ class AtprotoOAuthService {
throw new Error('Failed to initialize OAuth client');
}
- console.log('OAuth client ready, initializing to process callback...');
+
// Call init() again to process the callback URL
const result = await this.oauthClient.init();
- console.log('OAuth callback processing result:', result);
+
if (result?.session) {
// Process the session
@@ -339,47 +324,42 @@ class AtprotoOAuthService {
}
// If no session yet, wait a bit and try again
- console.log('No session found immediately, waiting...');
+
await new Promise(resolve => setTimeout(resolve, 1000));
// Try to check session again
const sessionCheck = await this.checkSession();
if (sessionCheck) {
- console.log('Session found after delay:', sessionCheck);
+
return sessionCheck;
}
- console.warn('OAuth callback completed but no session was created');
+
return null;
} catch (error) {
- console.error('OAuth callback handling failed:', error);
- console.error('Error details:', {
- name: error.name,
- message: error.message,
- stack: error.stack
- });
+
throw new Error(`OAuth認証の完了に失敗しました: ${error.message}`);
}
}
async checkSession(): Promise<{ did: string; handle: string } | null> {
try {
- console.log('=== CHECK SESSION CALLED ===');
+
if (!this.oauthClient) {
- console.log('No OAuth client, initializing...');
+
await this.initialize();
}
if (!this.oauthClient) {
- console.log('OAuth client initialization failed');
+
return null;
}
- console.log('Running oauthClient.init() to check session...');
+
const result = await this.oauthClient.init();
- console.log('oauthClient.init() result:', result);
+
if (result?.session) {
// Use the common session processing method
@@ -388,7 +368,7 @@ class AtprotoOAuthService {
return null;
} catch (error) {
- console.error('Session check failed:', error);
+
return null;
}
}
@@ -398,13 +378,7 @@ class AtprotoOAuthService {
}
getSession(): AtprotoSession | null {
- console.log('getSession called');
- console.log('Current state:', {
- hasAgent: !!this.agent,
- hasAgentSession: !!this.agent?.session,
- hasOAuthClient: !!this.oauthClient,
- hasSessionInfo: !!(this as any)._sessionInfo
- });
+
// First check if we have an agent with session
if (this.agent?.session) {
@@ -414,7 +388,7 @@ class AtprotoOAuthService {
accessJwt: this.agent.session.accessJwt || '',
refreshJwt: this.agent.session.refreshJwt || '',
};
- console.log('Returning agent session:', session);
+
return session;
}
@@ -426,11 +400,11 @@ class AtprotoOAuthService {
accessJwt: 'dpop-protected', // Indicate that tokens are handled by dpopFetch
refreshJwt: 'dpop-protected',
};
- console.log('Returning stored session info:', session);
+
return session;
}
- console.log('No session available');
+
return null;
}
@@ -450,28 +424,28 @@ class AtprotoOAuthService {
async logout(): Promise {
try {
- console.log('=== LOGGING OUT ===');
+
// Clear Agent
this.agent = null;
- console.log('Agent cleared');
+
// Clear BrowserOAuthClient session
if (this.oauthClient) {
- console.log('Clearing OAuth client session...');
+
try {
// BrowserOAuthClient may have a revoke or signOut method
if (typeof (this.oauthClient as any).signOut === 'function') {
await (this.oauthClient as any).signOut();
- console.log('OAuth client signed out');
+
} else if (typeof (this.oauthClient as any).revoke === 'function') {
await (this.oauthClient as any).revoke();
- console.log('OAuth client revoked');
+
} else {
- console.log('No explicit signOut method found on OAuth client');
+
}
} catch (oauthError) {
- console.error('OAuth client logout error:', oauthError);
+
}
// Reset the OAuth client to force re-initialization
@@ -492,11 +466,11 @@ class AtprotoOAuthService {
}
}
keysToRemove.forEach(key => {
- console.log('Removing localStorage key:', key);
+
localStorage.removeItem(key);
});
- console.log('=== LOGOUT COMPLETED ===');
+
// Force page reload to ensure clean state
setTimeout(() => {
@@ -504,7 +478,7 @@ class AtprotoOAuthService {
}, 100);
} catch (error) {
- console.error('Logout failed:', error);
+
}
}
@@ -519,8 +493,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did;
try {
- console.log('Saving cards to atproto collection...');
- console.log('Using DID:', did);
+
+
// Ensure we have a fresh agent
if (!this.agent) {
@@ -550,13 +524,6 @@ class AtprotoOAuthService {
createdAt: createdAt
};
- console.log('PutRecord request:', {
- repo: did,
- collection: collection,
- rkey: rkey,
- record: record
- });
-
// Use Agent's com.atproto.repo.putRecord method
const response = await this.agent.com.atproto.repo.putRecord({
@@ -566,9 +533,9 @@ class AtprotoOAuthService {
record: record
});
- console.log('カードデータをai.card.boxに保存しました:', response);
+
} catch (error) {
- console.error('カードボックス保存エラー:', error);
+
throw error;
}
}
@@ -584,8 +551,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did;
try {
- console.log('Fetching cards from atproto collection...');
- console.log('Using DID:', did);
+
+
// Ensure we have a fresh agent
if (!this.agent) {
@@ -598,7 +565,7 @@ class AtprotoOAuthService {
rkey: 'self'
});
- console.log('Cards from box response:', response);
+
// Convert to expected format
const result = {
@@ -611,7 +578,7 @@ class AtprotoOAuthService {
return result;
} catch (error) {
- console.error('カードボックス取得エラー:', error);
+
// If record doesn't exist, return empty
if (error.toString().includes('RecordNotFound')) {
@@ -633,8 +600,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did;
try {
- console.log('Deleting card box collection...');
- console.log('Using DID:', did);
+
+
// Ensure we have a fresh agent
if (!this.agent) {
@@ -647,33 +614,35 @@ class AtprotoOAuthService {
rkey: 'self'
});
- console.log('Card box deleted successfully:', response);
+
} catch (error) {
- console.error('カードボックス削除エラー:', error);
+
throw error;
}
}
// 手動でトークンを設定(開発・デバッグ用)
setManualTokens(accessJwt: string, refreshJwt: string): void {
- console.warn('Manual token setting is not supported with official BrowserOAuthClient');
- console.warn('Please use the proper OAuth flow instead');
+
+
// For backward compatibility, store in localStorage
+ const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:unknown';
+ const appHost = import.meta.env.VITE_APP_HOST || 'https://example.com';
const session: AtprotoSession = {
- did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn',
- handle: 'syui.ai',
+ did: adminDid,
+ handle: new URL(appHost).hostname,
accessJwt: accessJwt,
refreshJwt: refreshJwt
};
localStorage.setItem('atproto_session', JSON.stringify(session));
- console.log('Manual tokens stored in localStorage for backward compatibility');
+
}
// 後方互換性のための従来関数
saveSessionToStorage(session: AtprotoSession): void {
- console.warn('saveSessionToStorage is deprecated with BrowserOAuthClient');
+
localStorage.setItem('atproto_session', JSON.stringify(session));
}
diff --git a/oauth/src/utils/oauth-endpoints.ts b/oauth/src/utils/oauth-endpoints.ts
index 9e7ab1a..62cada2 100644
--- a/oauth/src/utils/oauth-endpoints.ts
+++ b/oauth/src/utils/oauth-endpoints.ts
@@ -53,7 +53,6 @@ export class OAuthEndpointHandler {
}
});
} catch (error) {
- console.error('Failed to generate JWKS:', error);
return new Response(JSON.stringify({ error: 'Failed to generate JWKS' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
@@ -62,7 +61,6 @@ export class OAuthEndpointHandler {
}
} catch (e) {
// If URL parsing fails, pass through to original fetch
- console.debug('URL parsing failed, passing through:', e);
}
// Pass through all other requests
@@ -136,6 +134,5 @@ export function registerOAuthServiceWorker() {
const blob = new Blob([swCode], { type: 'application/javascript' });
const swUrl = URL.createObjectURL(blob);
- navigator.serviceWorker.register(swUrl).catch(console.error);
}
}
\ No newline at end of file
diff --git a/oauth/src/utils/oauth-keys.ts b/oauth/src/utils/oauth-keys.ts
index 9be43d6..5ac150a 100644
--- a/oauth/src/utils/oauth-keys.ts
+++ b/oauth/src/utils/oauth-keys.ts
@@ -37,7 +37,6 @@ export class OAuthKeyManager {
this.keyPair = await this.importKeyPair(keyData);
return this.keyPair;
} catch (error) {
- console.warn('Failed to load stored key, generating new one:', error);
localStorage.removeItem('oauth_private_key');
}
}
@@ -115,7 +114,6 @@ export class OAuthKeyManager {
const privateKey = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey);
localStorage.setItem('oauth_private_key', JSON.stringify(privateKey));
} catch (error) {
- console.error('Failed to store private key:', error);
}
}
diff --git a/src/commands/oauth.rs b/src/commands/oauth.rs
index 6f02ae0..f220697 100644
--- a/src/commands/oauth.rs
+++ b/src/commands/oauth.rs
@@ -49,49 +49,46 @@ pub async fn build(project_dir: PathBuf) -> Result<()> {
.and_then(|v| v.as_str())
.unwrap_or("ai.syui.log");
- // Extract AI config if present
- let ai_config = config.get("ai")
- .and_then(|v| v.as_table());
-
- let ai_enabled = ai_config
- .and_then(|ai| ai.get("enabled"))
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- let ai_ask_ai = ai_config
- .and_then(|ai| ai.get("ask_ai"))
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- let ai_provider = ai_config
- .and_then(|ai| ai.get("provider"))
- .and_then(|v| v.as_str())
- .unwrap_or("ollama");
-
- let ai_model = ai_config
- .and_then(|ai| ai.get("model"))
- .and_then(|v| v.as_str())
- .unwrap_or("gemma2:2b");
-
- let ai_host = ai_config
- .and_then(|ai| ai.get("host"))
- .and_then(|v| v.as_str())
- .unwrap_or("https://ollama.syui.ai");
-
- let ai_system_prompt = ai_config
- .and_then(|ai| ai.get("system_prompt"))
- .and_then(|v| v.as_str())
- .unwrap_or("you are a helpful ai assistant");
-
+ // Extract AI configuration from ai config if available
+ let ai_config = config.get("ai").and_then(|v| v.as_table());
let ai_did = ai_config
- .and_then(|ai| ai.get("ai_did"))
+ .and_then(|ai_table| ai_table.get("ai_did"))
.and_then(|v| v.as_str())
.unwrap_or("did:plc:4hqjfn7m6n5hno3doamuhgef");
+ let ai_enabled = ai_config
+ .and_then(|ai_table| ai_table.get("enabled"))
+ .and_then(|v| v.as_bool())
+ .unwrap_or(true);
+ let ai_ask_ai = ai_config
+ .and_then(|ai_table| ai_table.get("ask_ai"))
+ .and_then(|v| v.as_bool())
+ .unwrap_or(true);
+ let ai_provider = ai_config
+ .and_then(|ai_table| ai_table.get("provider"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("ollama");
+ let ai_model = ai_config
+ .and_then(|ai_table| ai_table.get("model"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("gemma3:4b");
+ let ai_host = ai_config
+ .and_then(|ai_table| ai_table.get("host"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("https://ollama.syui.ai");
+ let ai_system_prompt = ai_config
+ .and_then(|ai_table| ai_table.get("system_prompt"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。");
// Extract bsky_api from oauth config
let bsky_api = oauth_config.get("bsky_api")
.and_then(|v| v.as_str())
.unwrap_or("https://public.api.bsky.app");
+
+ // Extract atproto_api from oauth config
+ let atproto_api = oauth_config.get("atproto_api")
+ .and_then(|v| v.as_str())
+ .unwrap_or("https://bsky.social");
// 4. Create .env.production content
let env_content = format!(
@@ -101,7 +98,7 @@ VITE_OAUTH_CLIENT_ID={}/{}
VITE_OAUTH_REDIRECT_URI={}/{}
VITE_ADMIN_DID={}
-# Base collection for OAuth app and ailog (all others are derived)
+# Base collection (all others are derived via getCollectionNames)
VITE_OAUTH_COLLECTION={}
# AI Configuration
@@ -115,6 +112,7 @@ VITE_AI_DID={}
# API Configuration
VITE_BSKY_PUBLIC_API={}
+VITE_ATPROTO_API={}
"#,
base_url,
base_url, client_id_path,
@@ -128,7 +126,8 @@ VITE_BSKY_PUBLIC_API={}
ai_host,
ai_system_prompt,
ai_did,
- bsky_api
+ bsky_api,
+ atproto_api
);
// 5. Find oauth directory (relative to current working directory)
diff --git a/src/commands/stream.rs b/src/commands/stream.rs
index 2f2ecbd..6f1a7aa 100644
--- a/src/commands/stream.rs
+++ b/src/commands/stream.rs
@@ -14,6 +14,29 @@ use reqwest;
use super::auth::{load_config, load_config_with_refresh, AuthConfig};
+#[derive(Debug, Clone)]
+struct AiConfig {
+ blog_host: String,
+ ollama_host: String,
+ ai_did: String,
+ model: String,
+ system_prompt: String,
+ bsky_api: String,
+}
+
+impl Default for AiConfig {
+ fn default() -> Self {
+ Self {
+ blog_host: "https://syui.ai".to_string(),
+ ollama_host: "https://ollama.syui.ai".to_string(),
+ ai_did: "did:plc:4hqjfn7m6n5hno3doamuhgef".to_string(),
+ model: "gemma3:4b".to_string(),
+ system_prompt: "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。".to_string(),
+ bsky_api: "https://public.api.bsky.app".to_string(),
+ }
+ }
+}
+
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct BlogPost {
@@ -112,6 +135,83 @@ fn load_collection_config_from_project(project_dir: &Path) -> Result<(String, St
Ok((collection_base, collection_user))
}
+// Load AI config from project's config.toml
+fn load_ai_config_from_project() -> Result {
+ // Try to find config.toml in current directory or parent directories
+ let mut current_dir = std::env::current_dir()?;
+ let mut config_path = None;
+
+ for _ in 0..5 { // Search up to 5 levels up
+ let potential_config = current_dir.join("config.toml");
+ if potential_config.exists() {
+ config_path = Some(potential_config);
+ break;
+ }
+ if !current_dir.pop() {
+ break;
+ }
+ }
+
+ let config_path = config_path.ok_or_else(|| anyhow::anyhow!("config.toml not found in current directory or parent directories"))?;
+
+ let config_content = fs::read_to_string(&config_path)
+ .with_context(|| format!("Failed to read config.toml from {}", config_path.display()))?;
+
+ let config: toml::Value = config_content.parse()
+ .with_context(|| "Failed to parse config.toml")?;
+
+ // Extract site config
+ let site_config = config.get("site").and_then(|v| v.as_table());
+ let blog_host = site_config
+ .and_then(|s| s.get("base_url"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("https://syui.ai")
+ .to_string();
+
+ // Extract AI config
+ let ai_config = config.get("ai").and_then(|v| v.as_table());
+ let ollama_host = ai_config
+ .and_then(|ai| ai.get("host"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("https://ollama.syui.ai")
+ .to_string();
+
+ let ai_did = ai_config
+ .and_then(|ai| ai.get("ai_did"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("did:plc:4hqjfn7m6n5hno3doamuhgef")
+ .to_string();
+
+ let model = ai_config
+ .and_then(|ai| ai.get("model"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("gemma3:4b")
+ .to_string();
+
+ let system_prompt = ai_config
+ .and_then(|ai| ai.get("system_prompt"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。")
+ .to_string();
+
+ // Extract OAuth config for bsky_api
+ let oauth_config = config.get("oauth").and_then(|v| v.as_table());
+ let bsky_api = oauth_config
+ .and_then(|oauth| oauth.get("bsky_api"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("https://public.api.bsky.app")
+ .to_string();
+
+ Ok(AiConfig {
+ blog_host,
+ ollama_host,
+ ai_did,
+ model,
+ system_prompt,
+ bsky_api,
+ })
+}
+
#[derive(Debug, Serialize, Deserialize)]
struct JetstreamMessage {
collection: Option,
@@ -432,6 +532,7 @@ async fn handle_message(text: &str, config: &mut AuthConfig) -> Result<()> {
async fn resolve_handle(did: &str) -> Result {
let client = reqwest::Client::new();
+ // Use default bsky API for handle resolution
let url = format!("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor={}",
urlencoding::encode(did));
@@ -931,27 +1032,51 @@ pub async fn test_api() -> Result<()> {
}
// AI content generation functions
-async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str) -> Result {
- let model = "gemma3:4b";
+async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiConfig) -> Result {
+ let model = &ai_config.model;
+ let system_prompt = &ai_config.system_prompt;
let prompt = match prompt_type {
- "translate" => format!("Translate the following Japanese blog post to English. Keep the technical terms and code blocks intact:\n\n{}", content),
- "comment" => format!("Read this blog post and provide an insightful comment about it. Focus on the key points and add your perspective:\n\n{}", content),
+ "translate" => format!(
+ "{}\n\n# 指示\n以下の日本語ブログ記事を英語に翻訳してください。\n- 技術用語やコードブロックはそのまま維持\n- アイらしい表現で翻訳\n- 簡潔に要点をまとめる\n\n# ブログ記事\n{}",
+ system_prompt, content
+ ),
+ "comment" => {
+ // Limit content to first 500 characters to reduce input size
+ let limited_content = if content.len() > 500 {
+ format!("{}...", &content[..500])
+ } else {
+ content.to_string()
+ };
+
+ format!(
+ "{}\n\n# 指示\nこのブログ記事を読んで、アイらしい感想を一言でください。\n- 30文字以内の短い感想\n- 技術的な内容への素朴な驚きや発見\n- 「わー!」「すごい!」など、アイらしい感嘆詞で始める\n- 簡潔で分かりやすく\n\n# ブログ記事(要約)\n{}\n\n# 出力形式\n一言の感想のみ(説明や詳細は不要):",
+ system_prompt, limited_content
+ )
+ },
_ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)),
};
+ let num_predict = match prompt_type {
+ "comment" => 50, // Very short for comments (about 30-40 characters)
+ "translate" => 3000, // Much longer for translations
+ _ => 300,
+ };
+
let request = OllamaRequest {
model: model.to_string(),
prompt,
stream: false,
options: OllamaOptions {
- temperature: 0.9,
- top_p: 0.9,
- num_predict: 500,
+ temperature: 0.7, // Lower temperature for more focused responses
+ top_p: 0.8,
+ num_predict,
},
};
- let client = reqwest::Client::new();
+ let client = reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(120)) // 2 minute timeout
+ .build()?;
// Try localhost first (for same-server deployment)
let localhost_url = "http://localhost:11434/api/generate";
@@ -967,8 +1092,14 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str
}
// Fallback to remote host
- let remote_url = format!("{}/api/generate", ollama_host);
- let response = client.post(&remote_url).json(&request).send().await?;
+ let remote_url = format!("{}/api/generate", ai_config.ollama_host);
+ println!("{}", format!("🔗 Making request to: {} with Origin: {}", remote_url, ai_config.blog_host).blue());
+ let response = client
+ .post(&remote_url)
+ .header("Origin", &ai_config.blog_host)
+ .json(&request)
+ .send()
+ .await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("Ollama API request failed: {}", response.status()));
@@ -980,9 +1111,15 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str
}
async fn run_ai_generation_monitor(config: &AuthConfig) -> Result<()> {
- let blog_host = "https://syui.ai"; // TODO: Load from config
- let ollama_host = "https://ollama.syui.ai"; // TODO: Load from config
- let ai_did = "did:plc:4hqjfn7m6n5hno3doamuhgef"; // TODO: Load from config
+ // Load AI config from project config.toml or use defaults
+ let ai_config = load_ai_config_from_project().unwrap_or_else(|e| {
+ println!("{}", format!("⚠️ Failed to load AI config: {}, using defaults", e).yellow());
+ AiConfig::default()
+ });
+
+ let blog_host = &ai_config.blog_host;
+ let ollama_host = &ai_config.ollama_host;
+ let ai_did = &ai_config.ai_did;
println!("{}", "🤖 Starting AI content generation monitor...".cyan());
println!("📡 Blog host: {}", blog_host);
@@ -998,7 +1135,7 @@ async fn run_ai_generation_monitor(config: &AuthConfig) -> Result<()> {
println!("{}", "🔍 Checking for new blog posts...".blue());
- match check_and_process_new_posts(&client, config, blog_host, ollama_host, ai_did).await {
+ match check_and_process_new_posts(&client, config, &ai_config).await {
Ok(count) => {
if count > 0 {
println!("{}", format!("✅ Processed {} new posts", count).green());
@@ -1018,12 +1155,10 @@ async fn run_ai_generation_monitor(config: &AuthConfig) -> Result<()> {
async fn check_and_process_new_posts(
client: &reqwest::Client,
config: &AuthConfig,
- blog_host: &str,
- ollama_host: &str,
- ai_did: &str,
+ ai_config: &AiConfig,
) -> Result {
// Fetch blog index
- let index_url = format!("{}/index.json", blog_host);
+ let index_url = format!("{}/index.json", ai_config.blog_host);
let response = client.get(&index_url).send().await?;
if !response.status().is_success() {
@@ -1042,25 +1177,57 @@ async fn check_and_process_new_posts(
for post in blog_posts {
let post_slug = extract_slug_from_url(&post.href);
- // Check if translation already exists
+ // Check if translation already exists (support both old and new format)
let translation_exists = existing_lang_records.iter().any(|record| {
- record.get("value")
+ let value = record.get("value");
+
+ // Check new format: value.post.slug
+ let new_format_match = value
+ .and_then(|v| v.get("post"))
+ .and_then(|p| p.get("slug"))
+ .and_then(|s| s.as_str())
+ == Some(&post_slug);
+
+ // Check old format: value.post_slug
+ let old_format_match = value
.and_then(|v| v.get("post_slug"))
.and_then(|s| s.as_str())
- == Some(&post_slug)
+ == Some(&post_slug);
+
+ new_format_match || old_format_match
});
- // Check if comment already exists
+ if translation_exists {
+ println!("{}", format!("⏭️ Translation already exists for: {}", post.title).yellow());
+ }
+
+ // Check if comment already exists (support both old and new format)
let comment_exists = existing_comment_records.iter().any(|record| {
- record.get("value")
+ let value = record.get("value");
+
+ // Check new format: value.post.slug
+ let new_format_match = value
+ .and_then(|v| v.get("post"))
+ .and_then(|p| p.get("slug"))
+ .and_then(|s| s.as_str())
+ == Some(&post_slug);
+
+ // Check old format: value.post_slug
+ let old_format_match = value
.and_then(|v| v.get("post_slug"))
.and_then(|s| s.as_str())
- == Some(&post_slug)
+ == Some(&post_slug);
+
+ new_format_match || old_format_match
});
+ if comment_exists {
+ println!("{}", format!("⏭️ Comment already exists for: {}", post.title).yellow());
+ }
+
// Generate translation if not exists
if !translation_exists {
- match generate_and_store_translation(client, config, &post, ollama_host, ai_did).await {
+ match generate_and_store_translation(client, config, &post, ai_config).await {
Ok(_) => {
println!("{}", format!("✅ Generated translation for: {}", post.title).green());
processed_count += 1;
@@ -1069,11 +1236,13 @@ async fn check_and_process_new_posts(
println!("{}", format!("❌ Failed to generate translation for {}: {}", post.title, e).red());
}
}
+ } else {
+ println!("{}", format!("⏭️ Translation already exists for: {}", post.title).yellow());
}
// Generate comment if not exists
if !comment_exists {
- match generate_and_store_comment(client, config, &post, ollama_host, ai_did).await {
+ match generate_and_store_comment(client, config, &post, ai_config).await {
Ok(_) => {
println!("{}", format!("✅ Generated comment for: {}", post.title).green());
processed_count += 1;
@@ -1082,6 +1251,8 @@ async fn check_and_process_new_posts(
println!("{}", format!("❌ Failed to generate comment for {}: {}", post.title, e).red());
}
}
+ } else {
+ println!("{}", format!("⏭️ Comment already exists for: {}", post.title).yellow());
}
}
@@ -1120,25 +1291,76 @@ fn extract_slug_from_url(url: &str) -> String {
.to_string()
}
+fn extract_date_from_slug(slug: &str) -> String {
+ // Extract date from slug like "2025-06-14-blog" -> "2025-06-14T00:00:00Z"
+ if slug.len() >= 10 && slug.chars().nth(4) == Some('-') && slug.chars().nth(7) == Some('-') {
+ format!("{}T00:00:00Z", &slug[0..10])
+ } else {
+ chrono::Utc::now().format("%Y-%m-%dT00:00:00Z").to_string()
+ }
+}
+
+async fn get_ai_profile(client: &reqwest::Client, ai_config: &AiConfig) -> Result {
+ let url = format!("{}/xrpc/app.bsky.actor.getProfile?actor={}",
+ ai_config.bsky_api, urlencoding::encode(&ai_config.ai_did));
+
+ let response = client
+ .get(&url)
+ .send()
+ .await?;
+
+ if !response.status().is_success() {
+ // Fallback to default AI profile
+ return Ok(serde_json::json!({
+ "did": ai_config.ai_did,
+ "handle": "yui.syui.ai",
+ "displayName": "ai",
+ "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hqjfn7m6n5hno3doamuhgef/bafkreiaxkv624mffw3cfyi67ufxtwuwsy2mjw2ygezsvtd44ycbgkfdo2a@jpeg"
+ }));
+ }
+
+ let profile_data: serde_json::Value = response.json().await?;
+
+ Ok(serde_json::json!({
+ "did": ai_config.ai_did,
+ "handle": profile_data["handle"].as_str().unwrap_or("yui.syui.ai"),
+ "displayName": profile_data["displayName"].as_str().unwrap_or("ai"),
+ "avatar": profile_data["avatar"].as_str()
+ }))
+}
+
async fn generate_and_store_translation(
client: &reqwest::Client,
config: &AuthConfig,
post: &BlogPost,
- ollama_host: &str,
- ai_did: &str,
+ ai_config: &AiConfig,
) -> Result<()> {
- // Generate translation
- let translation = generate_ai_content(&post.title, "translate", ollama_host).await?;
+ // Generate translation using post content instead of just title
+ let content_to_translate = format!("Title: {}\n\n{}", post.title, post.contents);
+ let translation = generate_ai_content(&content_to_translate, "translate", ai_config).await?;
- // Store in ai.syui.log.chat.lang collection
+ // Get AI profile information
+ let ai_author = get_ai_profile(client, ai_config).await?;
+
+ // Extract post metadata
+ let post_slug = extract_slug_from_url(&post.href);
+ let post_date = extract_date_from_slug(&post_slug);
+
+ // Store in ai.syui.log.chat.lang collection with new format
let record_data = serde_json::json!({
- "post_slug": extract_slug_from_url(&post.href),
- "post_title": post.title,
- "post_url": post.href,
- "lang": "en",
- "content": translation,
- "generated_at": chrono::Utc::now().to_rfc3339(),
- "ai_did": ai_did
+ "$type": "ai.syui.log.chat.lang",
+ "post": {
+ "url": post.href,
+ "slug": post_slug,
+ "title": post.title,
+ "date": post_date,
+ "tags": post.tags,
+ "language": "ja"
+ },
+ "type": "en",
+ "text": translation,
+ "author": ai_author,
+ "createdAt": chrono::Utc::now().to_rfc3339()
});
store_atproto_record(client, config, &config.collections.chat_lang(), &record_data).await
@@ -1148,20 +1370,39 @@ async fn generate_and_store_comment(
client: &reqwest::Client,
config: &AuthConfig,
post: &BlogPost,
- ollama_host: &str,
- ai_did: &str,
+ ai_config: &AiConfig,
) -> Result<()> {
- // Generate comment
- let comment = generate_ai_content(&post.title, "comment", ollama_host).await?;
+ // Generate comment using limited post content for brevity
+ let limited_contents = if post.contents.len() > 300 {
+ format!("{}...", &post.contents[..300])
+ } else {
+ post.contents.clone()
+ };
+ let content_to_comment = format!("Title: {}\n\n{}", post.title, limited_contents);
+ let comment = generate_ai_content(&content_to_comment, "comment", ai_config).await?;
- // Store in ai.syui.log.chat.comment collection
+ // Get AI profile information
+ let ai_author = get_ai_profile(client, ai_config).await?;
+
+ // Extract post metadata
+ let post_slug = extract_slug_from_url(&post.href);
+ let post_date = extract_date_from_slug(&post_slug);
+
+ // Store in ai.syui.log.chat.comment collection with new format
let record_data = serde_json::json!({
- "post_slug": extract_slug_from_url(&post.href),
- "post_title": post.title,
- "post_url": post.href,
- "content": comment,
- "generated_at": chrono::Utc::now().to_rfc3339(),
- "ai_did": ai_did
+ "$type": "ai.syui.log.chat.comment",
+ "post": {
+ "url": post.href,
+ "slug": post_slug,
+ "title": post.title,
+ "date": post_date,
+ "tags": post.tags,
+ "language": "ja"
+ },
+ "type": "info",
+ "text": comment,
+ "author": ai_author,
+ "createdAt": chrono::Utc::now().to_rfc3339()
});
store_atproto_record(client, config, &config.collections.chat_comment(), &record_data).await
@@ -1169,10 +1410,13 @@ async fn generate_and_store_comment(
async fn store_atproto_record(
client: &reqwest::Client,
- config: &AuthConfig,
+ _config: &AuthConfig,
collection: &str,
record_data: &serde_json::Value,
) -> Result<()> {
+ // Always load fresh config to ensure we have valid tokens
+ let config = load_config_with_refresh().await?;
+
let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds);
let put_request = serde_json::json!({