fix listrecord-created-sort

This commit is contained in:
2025-08-09 16:50:47 +09:00
parent e1eab122c8
commit 0d90ba21e0
7 changed files with 200 additions and 97 deletions

View File

@@ -48,7 +48,7 @@ export default function App() {
const records = await agent.api.com.atproto.repo.listRecords({ const records = await agent.api.com.atproto.repo.listRecords({
repo: user.did, repo: user.did,
collection: 'ai.syui.log.chat', collection: 'ai.syui.log.chat',
limit: 50 limit: 100
}) })
// Group questions and answers together // Group questions and answers together
@@ -83,8 +83,8 @@ export default function App() {
} }
}) })
// Sort by creation time (newest first) // Sort by creation time (oldest first) - for chronological conversation flow
chatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) chatPairs.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
setUserChatRecords(chatPairs) setUserChatRecords(chatPairs)
} catch (error) { } catch (error) {

View File

@@ -83,14 +83,11 @@ export const atproto = {
return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`) 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}` let url = `${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}`
if (cursor) { if (cursor) {
url += `&cursor=${cursor}` url += `&cursor=${cursor}`
} }
if (reverse) {
url += `&reverse=true`
}
const res = await request(url) const res = await request(url)
return { return {
records: res.records || [], records: res.records || [],
@@ -118,6 +115,48 @@ export const atproto = {
// Use Agent's putRecord method instead of direct fetch // Use Agent's putRecord method instead of direct fetch
return await agent.com.atproto.repo.putRecord(record) 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) const cached = dataCache.get(cacheKey)
if (cached) return cached 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 // Extract records array for backward compatibility
const records = data.records || data const records = data.records || data
dataCache.set(cacheKey, records) dataCache.set(cacheKey, records)
@@ -164,7 +203,7 @@ export const collections = {
async getChat(pds, repo, collection, limit = 10, cursor = null) { async getChat(pds, repo, collection, limit = 10, cursor = null) {
// Don't use cache for pagination requests // Don't use cache for pagination requests
if (cursor) { 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 return result
} }
@@ -175,7 +214,7 @@ export const collections = {
return Array.isArray(cached) ? { records: cached, cursor: null } : cached 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 // Cache only the records array for backward compatibility
dataCache.set(cacheKey, data.records || data) dataCache.set(cacheKey, data.records || data)
return data return data
@@ -217,6 +256,53 @@ export const collections = {
return records 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) { invalidateCache(collection) {
dataCache.invalidatePattern(collection) dataCache.invalidatePattern(collection)

View File

@@ -24,6 +24,24 @@ function getCorrectWebUrl(avatarUrl) {
export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat, apiConfig, user = null, agent = null, onRecordDeleted = null }) { export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat, apiConfig, user = null, agent = null, onRecordDeleted = null }) {
const [expandedRecords, setExpandedRecords] = useState(new Set()) 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 toggleJsonView = (key) => {
const newExpanded = new Set(expandedRecords) const newExpanded = new Set(expandedRecords)
@@ -35,7 +53,7 @@ export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat,
setExpandedRecords(newExpanded) setExpandedRecords(newExpanded)
} }
if (!chatPairs || chatPairs.length === 0) { if (!sortedChatPairs || sortedChatPairs.length === 0) {
return ( return (
<section> <section>
<p>チャット履歴がありません</p> <p>チャット履歴がありません</p>
@@ -84,7 +102,7 @@ export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat,
return ( return (
<section> <section>
{chatPairs.map((chatPair, i) => ( {sortedChatPairs.map((chatPair, i) => (
<div key={chatPair.rkey} className="chat-conversation"> <div key={chatPair.rkey} className="chat-conversation">
{/* Question */} {/* Question */}
{chatPair.question && ( {chatPair.question && (

View File

@@ -4,8 +4,17 @@ import ChatRecordList from './ChatRecordList.jsx'
import ProfileRecordList from './ProfileRecordList.jsx' import ProfileRecordList from './ProfileRecordList.jsx'
import LoadingSkeleton from './LoadingSkeleton.jsx' import LoadingSkeleton from './LoadingSkeleton.jsx'
import { logger } from '../utils/logger.js' 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 }) { 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) // 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 isAiPost = !pageContext.isTopPage && Array.isArray(chatRecords) && chatRecords.some(chatPair => {
const recordUrl = chatPair.question?.value?.post?.url 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') const [activeTab, setActiveTab] = useState(isAiPost ? 'collection' : 'profiles')
// Monitor activeTab changes // Fixed useEffect with proper dependency array
useEffect(() => { useEffect(() => {
logger.log('RecordTabs: activeTab changed to', activeTab) if (!pageContext.isTopPage && pageContext.rkey) {
}, [activeTab])
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 // Filter records based on page context
const filterRecords = (records, isProfile = false) => { const filterRecords = (records, isProfile = false) => {
// Ensure records is an array // Ensure records is an array
const recordsArray = Array.isArray(records) ? records : [] 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) { if (pageContext.isTopPage) {
// Top page: show latest 3 records // Top page: show latest 3 records
const result = recordsArray.slice(0, 3) return recordsArray.slice(0, 3)
logger.log('filterRecords: Top page result:', result.length, result)
return result
} else { } else {
// Individual page: show records matching the URL // Individual page: show records matching the URL
const filtered = recordsArray.filter(record => { const filtered = recordsArray.filter(record => {
// Profile records should always be shown // Profile records should always be shown
if (isProfile || record.value?.type === 'profile') { if (isProfile || record.value?.type === 'profile') {
logger.log('filterRecords: Profile record included:', record.value?.type)
return true return true
} }
const recordUrl = record.value?.post?.url const recordUrl = record.value?.post?.url
if (!recordUrl) { if (!recordUrl) {
logger.log('filterRecords: No recordUrl found for record:', record.value?.type)
return false return false
} }
try { try {
const recordRkey = new URL(recordUrl).pathname.split('/').pop()?.replace(/\.html$/, '') const recordRkey = new URL(recordUrl).pathname.split('/').pop()?.replace(/\.html$/, '')
const matches = recordRkey === pageContext.rkey return recordRkey === pageContext.rkey
logger.log('filterRecords: URL matching:', { recordRkey, pageRkey: pageContext.rkey, matches })
return matches
} catch { } catch {
logger.log('filterRecords: URL parsing failed for:', recordUrl)
return false return false
} }
}) })
logger.log('filterRecords: Individual page result:', filtered.length, filtered)
return filtered return filtered
} }
} }
@@ -82,25 +100,15 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
// Ensure chatPairs is an array // Ensure chatPairs is an array
const chatArray = Array.isArray(chatPairs) ? chatPairs : [] 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) { if (pageContext.isTopPage) {
// Top page: show latest 3 pairs // Top page: show latest 3 pairs
const result = chatArray.slice(0, 3) return chatArray.slice(0, 3)
logger.log('Top page: returning', result.length, 'pairs')
return result
} else { } else {
// Individual page: show pairs matching the URL (compare path only, ignore domain) // Individual page: show pairs matching the URL (compare path only, ignore domain)
const filtered = chatArray.filter(chatPair => { const filtered = chatArray.filter(chatPair => {
const recordUrl = chatPair.question?.value?.post?.url const recordUrl = chatPair.question?.value?.post?.url
if (!recordUrl) { if (!recordUrl) {
logger.log('No recordUrl for chatPair:', chatPair)
return false return false
} }
@@ -109,43 +117,25 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
const recordPath = new URL(recordUrl).pathname const recordPath = new URL(recordUrl).pathname
const recordRkey = recordPath.split('/').pop()?.replace(/\.html$/, '') const recordRkey = recordPath.split('/').pop()?.replace(/\.html$/, '')
logger.log('Comparing:', { recordRkey, pageRkey: pageContext.rkey, recordUrl })
// Compare with current page rkey // Compare with current page rkey
const matches = recordRkey === pageContext.rkey return recordRkey === pageContext.rkey
if (matches) {
logger.log('Found matching chat pair!')
}
return matches
} catch (error) { } catch (error) {
logger.log('Error processing recordUrl:', recordUrl, error)
return false return false
} }
}) })
logger.log('Individual page: returning', filtered.length, 'filtered pairs')
return filtered return filtered
} }
} }
const filteredLangRecords = filterRecords(Array.isArray(langRecords) ? langRecords : []) 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 : []) const filteredCommentRecords = filterRecords(Array.isArray(commentRecords) ? commentRecords : [])
logger.log('RecordTabs: After filtering commentRecords:', filteredCommentRecords.length, filteredCommentRecords)
const filteredUserComments = filterRecords(Array.isArray(userComments) ? userComments : []) const filteredUserComments = filterRecords(Array.isArray(userComments) ? userComments : [])
const filteredChatRecords = filterChatRecords(Array.isArray(chatRecords) ? chatRecords : []) const filteredChatRecords = filterChatRecords(Array.isArray(chatRecords) ? chatRecords : [])
const filteredBaseRecords = filterRecords(Array.isArray(baseRecords) ? baseRecords : []) 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 // Filter profile records from baseRecords
const profileRecords = (Array.isArray(baseRecords) ? baseRecords : []).filter(record => record.value?.type === 'profile') const profileRecords = (Array.isArray(baseRecords) ? baseRecords : []).filter(record => record.value?.type === 'profile')
@@ -162,10 +152,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
<div className="tab-header"> <div className="tab-header">
<button <button
className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`} className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`}
onClick={() => { onClick={() => setActiveTab('profiles')}
logger.log('RecordTabs: Profiles tab clicked')
setActiveTab('profiles')
}}
> >
about ({filteredProfileRecords.length}) about ({filteredProfileRecords.length})
</button> </button>
@@ -177,15 +164,9 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
</button> </button>
<button <button
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`} className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
onClick={() => { onClick={() => setActiveTab('comment')}
logger.log('RecordTabs: feedback tab clicked, setting activeTab to comment')
setActiveTab('comment')
}}
> >
feedback ({(() => { feedback ({filteredCommentRecords.length})
logger.log('RecordTabs: feedback tab render - filteredCommentRecords.length:', filteredCommentRecords.length)
return filteredCommentRecords.length
})()})
</button> </button>
<button <button
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`} className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
@@ -234,19 +215,33 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
) )
)} )}
{activeTab === 'collection' && ( {activeTab === 'collection' && (
userChatLoading ? ( (userChatLoading || pageSpecificLoading) ? (
<LoadingSkeleton count={2} showTitle={true} /> <LoadingSkeleton count={2} showTitle={true} />
) : ( ) : (() => {
<ChatRecordList const chatPairsToUse = !pageContext.isTopPage && pageSpecificChatRecords.length > 0
chatPairs={filteredChatRecords.length > 0 ? filteredChatRecords : (Array.isArray(userChatRecords) ? userChatRecords : [])} ? pageSpecificChatRecords
chatHasMore={filteredChatRecords.length > 0 ? chatHasMore : false} : (filteredChatRecords.length > 0 ? filteredChatRecords : (Array.isArray(userChatRecords) ? userChatRecords : []))
onLoadMoreChat={filteredChatRecords.length > 0 ? onLoadMoreChat : null}
apiConfig={apiConfig} return (
user={user} <ChatRecordList
agent={agent} chatPairs={chatPairsToUse}
onRecordDeleted={onRecordDeleted} chatHasMore={
/> !pageContext.isTopPage && pageSpecificChatRecords.length > 0
) ? false // Page-specific records don't use pagination
: (filteredChatRecords.length > 0 ? chatHasMore : false)
}
onLoadMoreChat={
!pageContext.isTopPage && pageSpecificChatRecords.length > 0
? null // Page-specific records don't use pagination
: (filteredChatRecords.length > 0 ? onLoadMoreChat : null)
}
apiConfig={apiConfig}
user={user}
agent={agent}
onRecordDeleted={onRecordDeleted}
/>
)
})()
)} )}
{activeTab === 'users' && !isAiPost && ( {activeTab === 'users' && !isAiPost && (
!userComments ? ( !userComments ? (

View File

@@ -48,7 +48,7 @@ export function useAdminData() {
logger.error('getComment error:', err) logger.error('getComment error:', err)
throw err throw err
}), }),
collections.getChat(apiConfig.pds, did, env.collection, 10).catch(err => { collections.getChat(apiConfig.pds, did, env.collection, 100).catch(err => {
logger.error('getChat error:', err) logger.error('getChat error:', err)
throw err throw err
}) })
@@ -98,8 +98,12 @@ export function useAdminData() {
} }
}) })
// Sort by creation time (newest first) // Sort by creation time (oldest first) - for chronological conversation flow
chatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) chatPairs.sort((a, b) => {
const dateA = new Date(a.createdAt)
const dateB = new Date(b.createdAt)
return dateA - dateB
})
logger.log('useAdminData: raw chat records:', chat.length) logger.log('useAdminData: raw chat records:', chat.length)
logger.log('useAdminData: processed chat pairs:', chatPairs.length, chatPairs) logger.log('useAdminData: processed chat pairs:', chatPairs.length, chatPairs)
@@ -128,7 +132,7 @@ export function useAdminData() {
try { try {
const apiConfig = getApiConfig(`https://${env.pds}`) const apiConfig = getApiConfig(`https://${env.pds}`)
const did = await atproto.getDid(env.pds, env.admin) const did = await atproto.getDid(env.pds, env.admin)
const chatResult = await collections.getChat(apiConfig.pds, did, env.collection, 10, chatCursor) const chatResult = await collections.getChat(apiConfig.pds, did, env.collection, 100, chatCursor)
const newChatRecords = chatResult.records || chatResult const newChatRecords = chatResult.records || chatResult
const newCursor = chatResult.cursor || null const newCursor = chatResult.cursor || null
@@ -168,8 +172,8 @@ export function useAdminData() {
} }
}) })
// Sort new pairs by creation time (newest first) // Sort new pairs by creation time (oldest first) - for chronological conversation flow
newChatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) newChatPairs.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
// Append to existing chat records // Append to existing chat records
setChatRecords(prev => [...prev, ...newChatPairs]) setChatRecords(prev => [...prev, ...newChatPairs])

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
export function usePageContext() { export function usePageContext() {
const [pageContext, setPageContext] = useState({ const [pageContext, setPageContext] = useState({

View File

@@ -1875,7 +1875,7 @@ async fn check_and_process_new_posts(
async fn get_existing_records(config: &AuthConfig, collection: &str) -> Result<Vec<serde_json::Value>> { async fn get_existing_records(config: &AuthConfig, collection: &str) -> Result<Vec<serde_json::Value>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let url = format!("{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit=100&reverse=true", let url = format!("{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit=100",
config.admin.pds, config.admin.pds,
urlencoding::encode(&config.admin.did), urlencoding::encode(&config.admin.did),
urlencoding::encode(collection)); urlencoding::encode(collection));