diff --git a/my-blog/static/css/style.css b/my-blog/static/css/style.css
index 84ae405..551993f 100644
--- a/my-blog/static/css/style.css
+++ b/my-blog/static/css/style.css
@@ -844,7 +844,6 @@ article.article-content {
font-size: 24px;
font-weight: 600;
margin-bottom: 32px;
- text-align: center;
}
/* OAuth Comment System - Hide on homepage by default, show on post pages */
diff --git a/oauth/package.json b/oauth/package.json
index 50b9e00..b52aac6 100644
--- a/oauth/package.json
+++ b/oauth/package.json
@@ -8,10 +8,13 @@
"preview": "vite preview"
},
"dependencies": {
+ "@atproto/api": "^0.15.12",
+ "@atproto/oauth-client-browser": "^0.3.19",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "@atproto/api": "^0.15.12",
- "@atproto/oauth-client-browser": "^0.3.19"
+ "react-markdown": "^9.0.1",
+ "rehype-highlight": "^7.0.2",
+ "remark-gfm": "^4.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
diff --git a/oauth/src/App.css b/oauth/src/App.css
index 703528b..54402e0 100644
--- a/oauth/src/App.css
+++ b/oauth/src/App.css
@@ -1337,10 +1337,144 @@ body {
.message-content {
color: var(--text);
line-height: 1.5;
- white-space: pre-wrap;
word-wrap: break-word;
}
+/* Markdown styles */
+.message-content h1,
+.message-content h2,
+.message-content h3,
+.message-content h4,
+.message-content h5,
+.message-content h6 {
+ margin: 16px 0 8px 0;
+ font-weight: 600;
+}
+
+.message-content h1 { font-size: 1.5em; }
+.message-content h2 { font-size: 1.3em; }
+.message-content h3 { font-size: 1.1em; }
+
+.message-content p {
+ margin: 8px 0;
+}
+
+.message-content pre {
+ background: var(--background-secondary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 12px;
+ margin: 12px 0;
+ overflow-x: auto;
+}
+
+.message-content code {
+ background: var(--background-secondary);
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace;
+ font-size: 0.9em;
+}
+
+.message-content pre code {
+ background: transparent;
+ padding: 0;
+ border-radius: 0;
+ font-size: 0.9em;
+}
+
+.message-content ul,
+.message-content ol {
+ margin: 8px 0;
+ padding-left: 24px;
+}
+
+.message-content li {
+ margin: 4px 0;
+}
+
+.message-content blockquote {
+ border-left: 4px solid var(--border);
+ padding-left: 16px;
+ margin: 12px 0;
+ color: var(--text-secondary);
+}
+
+.message-content table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 12px 0;
+}
+
+.message-content th,
+.message-content td {
+ border: 1px solid var(--border);
+ padding: 8px 12px;
+ text-align: left;
+}
+
+.message-content th {
+ background: var(--background-secondary);
+ font-weight: 600;
+}
+
+.message-content a {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+.message-content a:hover {
+ text-decoration: underline;
+}
+
+.message-content hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 16px 0;
+}
+
.record-actions {
flex-shrink: 0;
}
+
+.bluesky-footer {
+ text-align: center;
+ padding: 20px;
+ color: var(--primary);
+ font-size: 24px;
+}
+
+.bluesky-footer i {
+ transition: color 0.2s ease;
+}
+
+.bluesky-footer i:hover {
+ color: var(--primary-hover);
+}
+
+/* Custom code block styling */
+.message-content pre {
+ background: #2d3748 !important;
+ border: 1px solid #4a5568 !important;
+ border-radius: 6px;
+ padding: 12px;
+ margin: 12px 0;
+ overflow-x: auto;
+}
+
+.message-content pre code {
+ background: transparent !important;
+ color: #e2e8f0 !important;
+ font-family: 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.message-content code {
+ background: #2d3748 !important;
+ color: #e2e8f0 !important;
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
+ font-size: 14px;
+}
diff --git a/oauth/src/App.jsx b/oauth/src/App.jsx
index 2389745..7c0a801 100644
--- a/oauth/src/App.jsx
+++ b/oauth/src/App.jsx
@@ -14,7 +14,7 @@ import OAuthCallback from './components/OAuthCallback.jsx'
export default function App() {
const { user, agent, loading: authLoading, login, logout } = useAuth()
- const { adminData, langRecords, commentRecords, chatRecords: adminChatRecords, loading: dataLoading, error, refresh: refreshAdminData } = useAdminData()
+ const { adminData, langRecords, commentRecords, chatRecords: adminChatRecords, chatHasMore, loading: dataLoading, error, refresh: refreshAdminData, loadMoreChat } = useAdminData()
const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData)
const [userChatRecords, setUserChatRecords] = useState([])
const [userChatLoading, setUserChatLoading] = useState(false)
@@ -430,6 +430,8 @@ Answer:`
commentRecords={commentRecords}
userComments={userComments}
chatRecords={adminChatRecords}
+ chatHasMore={chatHasMore}
+ onLoadMoreChat={loadMoreChat}
userChatRecords={userChatRecords}
userChatLoading={userChatLoading}
baseRecords={adminData.records}
@@ -461,9 +463,6 @@ Answer:`
)}
-
-
-
diff --git a/oauth/src/api/atproto.js b/oauth/src/api/atproto.js
index 12836ac..2b8a792 100644
--- a/oauth/src/api/atproto.js
+++ b/oauth/src/api/atproto.js
@@ -83,9 +83,16 @@ export const atproto = {
return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
},
- async getRecords(pds, repo, collection, limit = 10) {
- const res = await request(`${pds}/xrpc/${ENDPOINTS.listRecords}?repo=${repo}&collection=${collection}&limit=${limit}`)
- return res.records || []
+ 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}`
+ }
+ const res = await request(url)
+ return {
+ records: res.records || [],
+ cursor: res.cursor || null
+ }
},
async searchPlc(plc, did) {
@@ -121,8 +128,10 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, collection, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
async getLang(pds, repo, collection, limit = 10) {
@@ -131,8 +140,10 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, `${collection}.chat.lang`, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
async getComment(pds, repo, collection, limit = 10) {
@@ -141,17 +152,29 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, `${collection}.chat.comment`, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
- async getChat(pds, repo, collection, limit = 10) {
+ 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)
+ return result
+ }
+
const cacheKey = dataCache.generateKey('chat', pds, repo, collection, limit)
const cached = dataCache.get(cacheKey)
- if (cached) return cached
+ if (cached) {
+ // Ensure cached data has the correct structure
+ return Array.isArray(cached) ? { records: cached, cursor: null } : cached
+ }
const data = await atproto.getRecords(pds, repo, `${collection}.chat`, limit)
- dataCache.set(cacheKey, data)
+ // Cache only the records array for backward compatibility
+ dataCache.set(cacheKey, data.records || data)
return data
},
@@ -161,8 +184,10 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, `${collection}.user`, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
async getUserComments(pds, repo, collection, limit = 10) {
@@ -171,8 +196,10 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, collection, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
async getProfiles(pds, repo, collection, limit = 100) {
@@ -181,8 +208,10 @@ export const collections = {
if (cached) return cached
const data = await atproto.getRecords(pds, repo, `${collection}.profile`, limit)
- dataCache.set(cacheKey, data)
- return data
+ // Extract records array for backward compatibility
+ const records = data.records || data
+ dataCache.set(cacheKey, records)
+ return records
},
// 投稿後にキャッシュを無効化
diff --git a/oauth/src/components/ChatRecordList.jsx b/oauth/src/components/ChatRecordList.jsx
index d2c4855..a6cfbf0 100644
--- a/oauth/src/components/ChatRecordList.jsx
+++ b/oauth/src/components/ChatRecordList.jsx
@@ -1,4 +1,8 @@
import React, { useState } from 'react'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import rehypeHighlight from 'rehype-highlight'
+import 'highlight.js/styles/github-dark.css'
// Helper function to get correct web URL based on avatar URL
function getCorrectWebUrl(avatarUrl) {
@@ -18,7 +22,7 @@ function getCorrectWebUrl(avatarUrl) {
return 'https://bsky.app'
}
-export default function ChatRecordList({ chatPairs, 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 toggleJsonView = (key) => {
@@ -139,7 +143,14 @@ export default function ChatRecordList({ chatPairs, apiConfig, user = null, agen
)}
- {chatPair.question.value.text}
+
+
+ {chatPair.question.value.text}
+
+
)}
@@ -190,25 +201,31 @@ export default function ChatRecordList({ chatPairs, apiConfig, user = null, agen
)}
- {chatPair.answer.value.text}
+
+
+ {chatPair.answer.value.text}
+
+
)}
- {/* Post metadata */}
- {chatPair.question?.value.post?.url && (
-
- )}
))}
+
+ {/* Load More Button */}
+ {chatHasMore && onLoadMoreChat && (
+
+
+
+ )}
)
}
\ No newline at end of file
diff --git a/oauth/src/components/RecordTabs.jsx b/oauth/src/components/RecordTabs.jsx
index 8a6de4d..dd9e89e 100644
--- a/oauth/src/components/RecordTabs.jsx
+++ b/oauth/src/components/RecordTabs.jsx
@@ -5,19 +5,22 @@ import ProfileRecordList from './ProfileRecordList.jsx'
import LoadingSkeleton from './LoadingSkeleton.jsx'
import { logger } from '../utils/logger.js'
-export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, 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 }) {
const [activeTab, setActiveTab] = useState('profiles')
logger.log('RecordTabs: activeTab is', activeTab)
// Filter records based on page context
const filterRecords = (records, isProfile = false) => {
+ // Ensure records is an array
+ const recordsArray = Array.isArray(records) ? records : []
+
if (pageContext.isTopPage) {
// Top page: show latest 3 records
- return records.slice(0, 3)
+ return recordsArray.slice(0, 3)
} else {
// Individual page: show records matching the URL
- return records.filter(record => {
+ return recordsArray.filter(record => {
// Profile records should always be shown
if (isProfile || record.value?.type === 'profile') {
return true
@@ -38,20 +41,25 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
// Special filter for chat records (which are already processed into pairs)
const filterChatRecords = (chatPairs) => {
+ // Ensure chatPairs is an array
+ const chatArray = Array.isArray(chatPairs) ? chatPairs : []
+
console.log('filterChatRecords called:', {
isTopPage: pageContext.isTopPage,
rkey: pageContext.rkey,
- chatPairsLength: chatPairs.length
+ chatPairsLength: chatArray.length,
+ chatPairsType: typeof chatPairs,
+ isArray: Array.isArray(chatPairs)
})
if (pageContext.isTopPage) {
// Top page: show latest 3 pairs
- const result = chatPairs.slice(0, 3)
+ const result = chatArray.slice(0, 3)
console.log('Top page: returning', result.length, 'pairs')
return result
} else {
// Individual page: show pairs matching the URL (compare path only, ignore domain)
- const filtered = chatPairs.filter(chatPair => {
+ const filtered = chatArray.filter(chatPair => {
const recordUrl = chatPair.question?.value?.post?.url
if (!recordUrl) {
console.log('No recordUrl for chatPair:', chatPair)
@@ -82,14 +90,14 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
}
}
- const filteredLangRecords = filterRecords(langRecords)
- const filteredCommentRecords = filterRecords(commentRecords)
- const filteredUserComments = filterRecords(userComments || [])
- const filteredChatRecords = filterChatRecords(chatRecords || [])
- const filteredBaseRecords = filterRecords(baseRecords || [])
+ const filteredLangRecords = filterRecords(Array.isArray(langRecords) ? langRecords : [])
+ const filteredCommentRecords = filterRecords(Array.isArray(commentRecords) ? commentRecords : [])
+ const filteredUserComments = filterRecords(Array.isArray(userComments) ? userComments : [])
+ const filteredChatRecords = filterChatRecords(Array.isArray(chatRecords) ? chatRecords : [])
+ const filteredBaseRecords = filterRecords(Array.isArray(baseRecords) ? baseRecords : [])
// Filter profile records from baseRecords
- const profileRecords = (baseRecords || []).filter(record => record.value?.type === 'profile')
+ const profileRecords = (Array.isArray(baseRecords) ? baseRecords : []).filter(record => record.value?.type === 'profile')
const sortedProfileRecords = profileRecords.sort((a, b) => {
if (a.value.profileType === 'admin' && b.value.profileType !== 'admin') return -1
if (a.value.profileType !== 'admin' && b.value.profileType === 'admin') return 1
@@ -171,7 +179,9 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
) : (
0 ? filteredChatRecords : userChatRecords}
+ 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}
diff --git a/oauth/src/hooks/useAdminData.js b/oauth/src/hooks/useAdminData.js
index 9e76736..65440b5 100644
--- a/oauth/src/hooks/useAdminData.js
+++ b/oauth/src/hooks/useAdminData.js
@@ -14,6 +14,8 @@ export function useAdminData() {
const [langRecords, setLangRecords] = useState([])
const [commentRecords, setCommentRecords] = useState([])
const [chatRecords, setChatRecords] = useState([])
+ const [chatCursor, setChatCursor] = useState(null)
+ const [chatHasMore, setChatHasMore] = useState(true)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -31,19 +33,30 @@ export function useAdminData() {
const profile = await atproto.getProfile(apiConfig.bsky, did)
// Load all data in parallel
- const [records, lang, comment, chat] = await Promise.all([
+ const [records, lang, comment, chatResult] = await Promise.all([
collections.getBase(apiConfig.pds, did, env.collection),
collections.getLang(apiConfig.pds, did, env.collection),
collections.getComment(apiConfig.pds, did, env.collection),
- collections.getChat(apiConfig.pds, did, env.collection)
+ collections.getChat(apiConfig.pds, did, env.collection, 10)
])
+
+ const chat = chatResult.records || chatResult
+ const cursor = chatResult.cursor || null
+ setChatCursor(cursor)
+ setChatHasMore(!!cursor)
+
+ console.log('useAdminData: chatResult structure:', chatResult)
+ console.log('useAdminData: chat variable type:', typeof chat, 'isArray:', Array.isArray(chat))
// Process chat records into question-answer pairs
const chatPairs = []
const recordMap = new Map()
+ // Ensure chat is an array
+ const chatArray = Array.isArray(chat) ? chat : []
+
// First pass: organize records by base rkey
- chat.forEach(record => {
+ chatArray.forEach(record => {
const rkey = record.uri.split('/').pop()
const baseRkey = rkey.replace('-answer', '')
@@ -88,13 +101,74 @@ export function useAdminData() {
}
}
+ const loadMoreChat = async () => {
+ if (!chatCursor || !chatHasMore) return
+
+ 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 newChatRecords = chatResult.records || chatResult
+ const newCursor = chatResult.cursor || null
+
+ // Process new chat records into question-answer pairs
+ const newChatPairs = []
+ const recordMap = new Map()
+
+ // Ensure newChatRecords is an array
+ const newChatArray = Array.isArray(newChatRecords) ? newChatRecords : []
+
+ // First pass: organize records by base rkey
+ newChatArray.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) {
+ newChatPairs.push({
+ rkey,
+ question: pair.question,
+ answer: pair.answer,
+ createdAt: pair.question.value.createdAt
+ })
+ }
+ })
+
+ // Sort new pairs by creation time (newest first)
+ newChatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+
+ // Append to existing chat records
+ setChatRecords(prev => [...prev, ...newChatPairs])
+ setChatCursor(newCursor)
+ setChatHasMore(!!newCursor)
+
+ } catch (err) {
+ // Silently fail - no error logging
+ }
+ }
+
return {
adminData,
langRecords,
commentRecords,
chatRecords,
+ chatHasMore,
loading,
error,
- refresh: loadAdminData
+ refresh: loadAdminData,
+ loadMoreChat
}
}
\ No newline at end of file
diff --git a/scpt/delete-chat-records.zsh b/scpt/delete-chat-records.zsh
index 4a5c3fd..09722b5 100755
--- a/scpt/delete-chat-records.zsh
+++ b/scpt/delete-chat-records.zsh
@@ -6,7 +6,7 @@ cb=ai.syui.log
cl=($cb.chat)
f=~/.config/syui/ai/log/config.json
-default_collection="ai.syui.log.chat"
+#default_collection="ai.syui.log.chat"
default_pds=syu.is
default_did=`cat $f|jq -r .admin.did`
default_token=`cat $f|jq -r .admin.access_jwt`