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({
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) {

View File

@@ -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)

View File

@@ -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 (
<section>
<p>チャット履歴がありません</p>
@@ -84,7 +102,7 @@ export default function ChatRecordList({ chatPairs, chatHasMore, onLoadMoreChat,
return (
<section>
{chatPairs.map((chatPair, i) => (
{sortedChatPairs.map((chatPair, i) => (
<div key={chatPair.rkey} className="chat-conversation">
{/* Question */}
{chatPair.question && (

View File

@@ -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,
<div className="tab-header">
<button
className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`}
onClick={() => {
logger.log('RecordTabs: Profiles tab clicked')
setActiveTab('profiles')
}}
onClick={() => setActiveTab('profiles')}
>
about ({filteredProfileRecords.length})
</button>
@@ -177,15 +164,9 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
</button>
<button
className={`tab-btn ${activeTab === 'comment' ? 'active' : ''}`}
onClick={() => {
logger.log('RecordTabs: feedback tab clicked, setting activeTab to comment')
setActiveTab('comment')
}}
onClick={() => setActiveTab('comment')}
>
feedback ({(() => {
logger.log('RecordTabs: feedback tab render - filteredCommentRecords.length:', filteredCommentRecords.length)
return filteredCommentRecords.length
})()})
feedback ({filteredCommentRecords.length})
</button>
<button
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
@@ -234,19 +215,33 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
)
)}
{activeTab === 'collection' && (
userChatLoading ? (
(userChatLoading || pageSpecificLoading) ? (
<LoadingSkeleton count={2} showTitle={true} />
) : (
<ChatRecordList
chatPairs={filteredChatRecords.length > 0 ? filteredChatRecords : (Array.isArray(userChatRecords) ? userChatRecords : [])}
chatHasMore={filteredChatRecords.length > 0 ? chatHasMore : false}
onLoadMoreChat={filteredChatRecords.length > 0 ? onLoadMoreChat : null}
apiConfig={apiConfig}
user={user}
agent={agent}
onRecordDeleted={onRecordDeleted}
/>
)
) : (() => {
const chatPairsToUse = !pageContext.isTopPage && pageSpecificChatRecords.length > 0
? pageSpecificChatRecords
: (filteredChatRecords.length > 0 ? filteredChatRecords : (Array.isArray(userChatRecords) ? userChatRecords : []))
return (
<ChatRecordList
chatPairs={chatPairsToUse}
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 && (
!userComments ? (

View File

@@ -48,7 +48,7 @@ export function useAdminData() {
logger.error('getComment error:', 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)
throw err
})
@@ -98,8 +98,12 @@ export function useAdminData() {
}
})
// 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) => {
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: processed chat pairs:', chatPairs.length, chatPairs)
@@ -128,7 +132,7 @@ export function useAdminData() {
try {
const apiConfig = getApiConfig(`https://${env.pds}`)
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 newCursor = chatResult.cursor || null
@@ -168,8 +172,8 @@ export function useAdminData() {
}
})
// Sort new pairs by creation time (newest first)
newChatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
// Sort new pairs by creation time (oldest first) - for chronological conversation flow
newChatPairs.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
// Append to existing chat records
setChatRecords(prev => [...prev, ...newChatPairs])

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
export function usePageContext() {
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>> {
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,
urlencoding::encode(&config.admin.did),
urlencoding::encode(collection));