diff --git a/oauth/src/App.jsx b/oauth/src/App.jsx index ba431ae..04c493a 100644 --- a/oauth/src/App.jsx +++ b/oauth/src/App.jsx @@ -48,7 +48,7 @@ export default function App() { const records = await agent.api.com.atproto.repo.listRecords({ repo: user.did, collection: 'ai.syui.log.chat', - limit: 50 + limit: 100 }) // Group questions and answers together @@ -83,8 +83,8 @@ export default function App() { } }) - // Sort by creation time (newest first) - chatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + // Sort by creation time (oldest first) - for chronological conversation flow + chatPairs.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) setUserChatRecords(chatPairs) } catch (error) { diff --git a/oauth/src/api/atproto.js b/oauth/src/api/atproto.js index e175b5f..980d175 100644 --- a/oauth/src/api/atproto.js +++ b/oauth/src/api/atproto.js @@ -83,14 +83,11 @@ export const atproto = { return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`) }, - async getRecords(pds, repo, collection, limit = 10, cursor = null, reverse = false) { + async getRecords(pds, repo, collection, limit = 10, cursor = null) { let url = `${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}` if (cursor) { url += `&cursor=${cursor}` } - if (reverse) { - url += `&reverse=true` - } const res = await request(url) return { records: res.records || [], @@ -118,6 +115,48 @@ export const atproto = { // Use Agent's putRecord method instead of direct fetch return await agent.com.atproto.repo.putRecord(record) + }, + + // Find all records for a specific post by paginating through all records + async findRecordsForPost(pds, repo, collection, targetRkey) { + let cursor = null + let allMatchingRecords = [] + let pageCount = 0 + const maxPages = 50 // Safety limit to prevent infinite loops + + do { + pageCount++ + if (pageCount > maxPages) { + console.warn(`Reached max pages (${maxPages}) while searching for ${targetRkey}`) + break + } + + const result = await this.getRecords(pds, repo, collection, 100, cursor) + + // Filter records that match the target post + const matchingRecords = result.records.filter(record => { + const postUrl = record.value?.post?.url + if (!postUrl) return false + + try { + // Extract rkey from URL + const recordRkey = new URL(postUrl).pathname.split('/').pop()?.replace(/\.html$/, '') + return recordRkey === targetRkey + } catch { + return false + } + }) + + allMatchingRecords.push(...matchingRecords) + cursor = result.cursor + + // Optional: Stop early if we found some records (uncomment if desired) + // if (allMatchingRecords.length > 0) break + + } while (cursor) + + console.log(`Found ${allMatchingRecords.length} records for ${targetRkey} after searching ${pageCount} pages`) + return allMatchingRecords } } @@ -154,7 +193,7 @@ export const collections = { const cached = dataCache.get(cacheKey) if (cached) return cached - const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit, null, true) // reverse=true for chronological order + const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit) // Extract records array for backward compatibility const records = data.records || data dataCache.set(cacheKey, records) @@ -164,7 +203,7 @@ export const collections = { async getChat(pds, repo, collection, limit = 10, cursor = null) { // Don't use cache for pagination requests if (cursor) { - const result = await atproto.getRecords(pds, repo, `${collection}.chat`, limit, cursor, true) // reverse=true for chronological order + const result = await atproto.getRecords(pds, repo, `${collection}.chat`, limit, cursor) return result } @@ -175,7 +214,7 @@ export const collections = { return Array.isArray(cached) ? { records: cached, cursor: null } : cached } - const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit, null, true) // reverse=true for chronological order + const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit) // Cache only the records array for backward compatibility dataCache.set(cacheKey, data.records || data) return data @@ -217,6 +256,53 @@ export const collections = { return records }, + // Find chat records for a specific post using pagination + async getChatForPost(pds, repo, collection, targetRkey) { + const cacheKey = dataCache.generateKey('chatForPost', pds, repo, collection, targetRkey) + const cached = dataCache.get(cacheKey) + if (cached) return cached + + const records = await atproto.findRecordsForPost(pds, repo, `${collection}.chat`, targetRkey) + + // Process into chat pairs like the original getChat function + const chatPairs = [] + const recordMap = new Map() + + // First pass: organize records by base rkey + records.forEach(record => { + const rkey = record.uri.split('/').pop() + const baseRkey = rkey.replace('-answer', '') + + if (!recordMap.has(baseRkey)) { + recordMap.set(baseRkey, { question: null, answer: null }) + } + + if (record.value.type === 'question') { + recordMap.get(baseRkey).question = record + } else if (record.value.type === 'answer') { + recordMap.get(baseRkey).answer = record + } + }) + + // Second pass: create chat pairs + recordMap.forEach((pair, rkey) => { + if (pair.question) { + chatPairs.push({ + rkey, + question: pair.question, + answer: pair.answer, + createdAt: pair.question.value.createdAt + }) + } + }) + + // Sort by creation time (oldest first) - for chronological conversation flow + chatPairs.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)) + + dataCache.set(cacheKey, chatPairs) + return chatPairs + }, + // 投稿後にキャッシュを無効化 invalidateCache(collection) { dataCache.invalidatePattern(collection) diff --git a/oauth/src/components/ChatRecordList.jsx b/oauth/src/components/ChatRecordList.jsx index a6cfbf0..e6a8b1e 100644 --- a/oauth/src/components/ChatRecordList.jsx +++ b/oauth/src/components/ChatRecordList.jsx @@ -24,6 +24,24 @@ function getCorrectWebUrl(avatarUrl) { export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat, apiConfig, user = null, agent = null, onRecordDeleted = null }) { const [expandedRecords, setExpandedRecords] = useState(new Set()) + + // Sort chat pairs by creation time (oldest first) for chronological conversation flow + const sortedChatPairs = Array.isArray(chatPairs) + ? [...chatPairs].sort((a, b) => { + const dateA = new Date(a.createdAt) + const dateB = new Date(b.createdAt) + + // If creation times are the same, sort by URI (which contains sequence info) + if (dateA.getTime() === dateB.getTime()) { + const uriA = a.question?.uri || '' + const uriB = b.question?.uri || '' + return uriA.localeCompare(uriB) + } + + return dateA - dateB + }) + : [] + const toggleJsonView = (key) => { const newExpanded = new Set(expandedRecords) @@ -35,7 +53,7 @@ export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat, setExpandedRecords(newExpanded) } - if (!chatPairs || chatPairs.length === 0) { + if (!sortedChatPairs || sortedChatPairs.length === 0) { return (

チャット履歴がありません

@@ -84,7 +102,7 @@ export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat, return (
- {chatPairs.map((chatPair, i) => ( + {sortedChatPairs.map((chatPair, i) => (
{/* Question */} {chatPair.question && ( diff --git a/oauth/src/components/RecordTabs.jsx b/oauth/src/components/RecordTabs.jsx index fbdb442..90d19d6 100644 --- a/oauth/src/components/RecordTabs.jsx +++ b/oauth/src/components/RecordTabs.jsx @@ -4,8 +4,17 @@ import ChatRecordList from './ChatRecordList.jsx' import ProfileRecordList from './ProfileRecordList.jsx' import LoadingSkeleton from './LoadingSkeleton.jsx' import { logger } from '../utils/logger.js' +import { collections } from '../api/atproto.js' +import { getApiConfig } from '../utils/pds.js' +import { env } from '../config/env.js' export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, chatHasMore, onLoadMoreChat, userChatRecords, userChatLoading, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) { + // State for page-specific chat records + const [pageSpecificChatRecords, setPageSpecificChatRecords] = useState([]) + const [pageSpecificLoading, setPageSpecificLoading] = useState(false) + + + // Check if current page has matching chat records (AI posts always have chat records) const isAiPost = !pageContext.isTopPage && Array.isArray(chatRecords) && chatRecords.some(chatPair => { const recordUrl = chatPair.question?.value?.post?.url @@ -20,59 +29,68 @@ export default function RecordTabs({ langRecords, commentRecords, userComments, }) const [activeTab, setActiveTab] = useState(isAiPost ? 'collection' : 'profiles') - - // Monitor activeTab changes + + // Fixed useEffect with proper dependency array useEffect(() => { - logger.log('RecordTabs: activeTab changed to', activeTab) - }, [activeTab]) + if (!pageContext.isTopPage && pageContext.rkey) { + + const fetchPageSpecificChats = async () => { + setPageSpecificLoading(true) + try { + const apiConfig = getApiConfig(`https://${env.pds}`) + const { atproto } = await import('../api/atproto.js') + const did = await atproto.getDid(env.pds, env.admin) + + const records = await collections.getChatForPost( + apiConfig.pds, + did, + env.collection, + pageContext.rkey + ) + setPageSpecificChatRecords(records) + } catch (error) { + setPageSpecificChatRecords([]) + } finally { + setPageSpecificLoading(false) + } + } + + fetchPageSpecificChats() + } else { + setPageSpecificChatRecords([]) + } + }, [pageContext.isTopPage, pageContext.rkey]) // Add proper dependencies - logger.log('RecordTabs: activeTab is', activeTab) - logger.log('RecordTabs: commentRecords prop:', commentRecords?.length || 0, commentRecords) // Filter records based on page context const filterRecords = (records, isProfile = false) => { // Ensure records is an array const recordsArray = Array.isArray(records) ? records : [] - logger.log('filterRecords called with:', { - recordsLength: recordsArray.length, - isProfile, - isTopPage: pageContext.isTopPage, - pageRkey: pageContext.rkey, - records: recordsArray - }) if (pageContext.isTopPage) { // Top page: show latest 3 records - const result = recordsArray.slice(0, 3) - logger.log('filterRecords: Top page result:', result.length, result) - return result + return recordsArray.slice(0, 3) } else { // Individual page: show records matching the URL const filtered = recordsArray.filter(record => { // Profile records should always be shown if (isProfile || record.value?.type === 'profile') { - logger.log('filterRecords: Profile record included:', record.value?.type) return true } const recordUrl = record.value?.post?.url if (!recordUrl) { - logger.log('filterRecords: No recordUrl found for record:', record.value?.type) return false } try { const recordRkey = new URL(recordUrl).pathname.split('/').pop()?.replace(/\.html$/, '') - const matches = recordRkey === pageContext.rkey - logger.log('filterRecords: URL matching:', { recordRkey, pageRkey: pageContext.rkey, matches }) - return matches + return recordRkey === pageContext.rkey } catch { - logger.log('filterRecords: URL parsing failed for:', recordUrl) return false } }) - logger.log('filterRecords: Individual page result:', filtered.length, filtered) return filtered } } @@ -82,25 +100,15 @@ export default function RecordTabs({ langRecords, commentRecords, userComments, // Ensure chatPairs is an array const chatArray = Array.isArray(chatPairs) ? chatPairs : [] - logger.log('filterChatRecords called:', { - isTopPage: pageContext.isTopPage, - rkey: pageContext.rkey, - chatPairsLength: chatArray.length, - chatPairsType: typeof chatPairs, - isArray: Array.isArray(chatPairs) - }) if (pageContext.isTopPage) { // Top page: show latest 3 pairs - const result = chatArray.slice(0, 3) - logger.log('Top page: returning', result.length, 'pairs') - return result + return chatArray.slice(0, 3) } else { // Individual page: show pairs matching the URL (compare path only, ignore domain) const filtered = chatArray.filter(chatPair => { const recordUrl = chatPair.question?.value?.post?.url if (!recordUrl) { - logger.log('No recordUrl for chatPair:', chatPair) return false } @@ -109,43 +117,25 @@ export default function RecordTabs({ langRecords, commentRecords, userComments, const recordPath = new URL(recordUrl).pathname const recordRkey = recordPath.split('/').pop()?.replace(/\.html$/, '') - logger.log('Comparing:', { recordRkey, pageRkey: pageContext.rkey, recordUrl }) - // Compare with current page rkey - const matches = recordRkey === pageContext.rkey - if (matches) { - logger.log('Found matching chat pair!') - } - return matches + return recordRkey === pageContext.rkey } catch (error) { - logger.log('Error processing recordUrl:', recordUrl, error) return false } }) - logger.log('Individual page: returning', filtered.length, 'filtered pairs') return filtered } } const filteredLangRecords = filterRecords(Array.isArray(langRecords) ? langRecords : []) - logger.log('RecordTabs: About to filter commentRecords:', commentRecords?.length || 0, commentRecords) const filteredCommentRecords = filterRecords(Array.isArray(commentRecords) ? commentRecords : []) - logger.log('RecordTabs: After filtering commentRecords:', filteredCommentRecords.length, filteredCommentRecords) const filteredUserComments = filterRecords(Array.isArray(userComments) ? userComments : []) const filteredChatRecords = filterChatRecords(Array.isArray(chatRecords) ? chatRecords : []) const filteredBaseRecords = filterRecords(Array.isArray(baseRecords) ? baseRecords : []) - logger.log('RecordTabs: filtered results:') - logger.log(' - filteredCommentRecords:', filteredCommentRecords.length, filteredCommentRecords) - logger.log(' - filteredLangRecords:', filteredLangRecords.length) - logger.log(' - filteredUserComments:', filteredUserComments.length) - logger.log(' - pageContext:', pageContext) - logger.log('RecordTabs: TAB RENDER VALUES:') - logger.log(' - filteredCommentRecords.length for tab:', filteredCommentRecords.length) - logger.log(' - commentRecords input:', commentRecords?.length || 0) // Filter profile records from baseRecords const profileRecords = (Array.isArray(baseRecords) ? baseRecords : []).filter(record => record.value?.type === 'profile') @@ -162,10 +152,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
@@ -177,15 +164,9 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,