From ecd69557fe00df75a90210791091a367b472d151 Mon Sep 17 00:00:00 2001 From: syui Date: Wed, 16 Jul 2025 20:42:50 +0900 Subject: [PATCH] oauth markdown --- my-blog/static/css/style.css | 1 - oauth/package.json | 7 +- oauth/src/App.css | 136 +++++++++++++++++++++++- oauth/src/App.jsx | 7 +- oauth/src/api/atproto.js | 65 +++++++---- oauth/src/components/ChatRecordList.jsx | 49 ++++++--- oauth/src/components/RecordTabs.jsx | 36 ++++--- oauth/src/hooks/useAdminData.js | 82 +++++++++++++- scpt/delete-chat-records.zsh | 2 +- 9 files changed, 325 insertions(+), 60 deletions(-) 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 && ( -
- - {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`