fix oauth package name

This commit is contained in:
2025-06-19 11:56:58 +09:00
parent b17ac3d91a
commit 30bdd7b633
105 changed files with 1116 additions and 8739 deletions

View File

@ -0,0 +1,69 @@
import { useState, useEffect } from 'react'
import { atproto, collections } from '../api/atproto.js'
import { getApiConfig } from '../utils/pds.js'
import { env } from '../config/env.js'
import { getErrorMessage, logError } from '../utils/errorHandler.js'
export function useAdminData() {
const [adminData, setAdminData] = useState({
did: '',
profile: null,
records: [],
apiConfig: null
})
const [langRecords, setLangRecords] = useState([])
const [commentRecords, setCommentRecords] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [retryCount, setRetryCount] = useState(0)
useEffect(() => {
loadAdminData()
}, [])
const loadAdminData = async () => {
try {
setLoading(true)
setError(null)
const apiConfig = getApiConfig(`https://${env.pds}`)
const did = await atproto.getDid(env.pds, env.admin)
const profile = await atproto.getProfile(apiConfig.bsky, did)
// Load all data in parallel
const [records, lang, comment] = await Promise.all([
collections.getBase(apiConfig.pds, did, env.collection),
collections.getLang(apiConfig.pds, did, env.collection),
collections.getComment(apiConfig.pds, did, env.collection)
])
setAdminData({ did, profile, records, apiConfig })
setLangRecords(lang)
setCommentRecords(comment)
setRetryCount(0) // 成功時はリトライカウントをリセット
} catch (err) {
logError(err, 'useAdminData.loadAdminData')
setError(getErrorMessage(err))
// 自動リトライ最大3回
if (retryCount < 3) {
setTimeout(() => {
setRetryCount(prev => prev + 1)
loadAdminData()
}, Math.pow(2, retryCount) * 1000) // 1s, 2s, 4s
}
} finally {
setLoading(false)
}
}
return {
adminData,
langRecords,
commentRecords,
loading,
error,
retryCount,
refresh: loadAdminData
}
}

234
oauth/src/hooks/useAskAI.js Normal file
View File

@ -0,0 +1,234 @@
import { useState } from 'react'
import { atproto, collections } from '../api/atproto.js'
import { env } from '../config/env.js'
import { logger } from '../utils/logger.js'
import { getErrorMessage, logError } from '../utils/errorHandler.js'
export function useAskAI(adminData, userProfile, agent) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [chatHistory, setChatHistory] = useState([])
// ask-AIサーバーのURL環境変数から取得、フォールバック付き
const askAIUrl = import.meta.env.VITE_ASK_AI_URL || 'http://localhost:3000/ask'
const askQuestion = async (question) => {
if (!question.trim()) return
setLoading(true)
setError(null)
try {
logger.log('Sending question to ask-AI:', question)
// ask-AIサーバーにリクエスト送信
const response = await fetch(askAIUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: question.trim(),
context: {
url: window.location.href,
timestamp: new Date().toISOString()
}
})
})
if (!response.ok) {
throw new Error(`ask-AI server error: ${response.status}`)
}
const aiResponse = await response.json()
logger.log('Received AI response:', aiResponse)
// AI回答をチャット履歴に追加
const chatEntry = {
id: `chat-${Date.now()}`,
question: question.trim(),
answer: aiResponse.answer || 'エラーが発生しました',
timestamp: new Date().toISOString(),
user: userProfile ? {
did: userProfile.did,
handle: userProfile.handle,
displayName: userProfile.displayName,
avatar: userProfile.avatar
} : null
}
setChatHistory(prev => [...prev, chatEntry])
// atprotoにレコードを保存
await saveChatRecord(chatEntry, aiResponse)
// Dispatch event for blog communication
window.dispatchEvent(new CustomEvent('aiResponseReceived', {
detail: {
question: chatEntry.question,
answer: chatEntry.answer,
timestamp: chatEntry.timestamp,
aiProfile: adminData?.profile ? {
did: adminData.did,
handle: adminData.profile.handle,
displayName: adminData.profile.displayName,
avatar: adminData.profile.avatar
} : null
}
}))
return aiResponse
} catch (err) {
logError(err, 'useAskAI.askQuestion')
setError(getErrorMessage(err))
throw err
} finally {
setLoading(false)
}
}
const saveChatRecord = async (chatEntry, aiResponse) => {
if (!agent || !adminData?.did) {
logger.warn('Cannot save chat record: missing agent or admin data')
return
}
try {
const currentUrl = window.location.href
const timestamp = chatEntry.timestamp
const baseRkey = `${new Date(timestamp).toISOString().replace(/[:.]/g, '-').slice(0, -5)}Z`
// Post metadata (共通)
const postMetadata = {
url: currentUrl,
date: timestamp,
slug: new URL(currentUrl).pathname.split('/').pop()?.replace(/\.html$/, '') || '',
tags: [],
title: document.title || 'AI Chat',
language: 'ja'
}
// Question record (ユーザーの質問)
const questionRecord = {
repo: adminData.did,
collection: `${env.collection}.chat`,
rkey: baseRkey,
record: {
$type: `${env.collection}.chat`,
post: postMetadata,
text: chatEntry.question,
type: 'question',
author: chatEntry.user ? {
did: chatEntry.user.did,
handle: chatEntry.user.handle,
displayName: chatEntry.user.displayName,
avatar: chatEntry.user.avatar
} : {
did: 'unknown',
handle: 'user',
displayName: 'User',
avatar: null
},
createdAt: timestamp
}
}
// Answer record (AIの回答)
const answerRecord = {
repo: adminData.did,
collection: `${env.collection}.chat`,
rkey: `${baseRkey}-answer`,
record: {
$type: `${env.collection}.chat`,
post: postMetadata,
text: chatEntry.answer,
type: 'answer',
author: {
did: adminData.did,
handle: adminData.profile?.handle || 'ai',
displayName: adminData.profile?.displayName || 'ai',
avatar: adminData.profile?.avatar || null
},
createdAt: timestamp
}
}
logger.log('Saving question record to atproto:', questionRecord)
await atproto.putRecord(null, questionRecord, agent)
logger.log('Saving answer record to atproto:', answerRecord)
await atproto.putRecord(null, answerRecord, agent)
// キャッシュを無効化
collections.invalidateCache(env.collection)
logger.log('Chat records saved successfully')
} catch (err) {
logError(err, 'useAskAI.saveChatRecord')
// 保存エラーは致命的ではないので、UIエラーにはしない
}
}
const clearChatHistory = () => {
setChatHistory([])
setError(null)
}
const loadChatHistory = async () => {
if (!adminData?.did) return
try {
const records = await collections.getChat(
adminData.apiConfig.pds,
adminData.did,
env.collection
)
// Group records by timestamp and create Q&A pairs
const recordGroups = {}
records.forEach(record => {
const timestamp = record.value.createdAt
const baseKey = timestamp.replace('-answer', '')
if (!recordGroups[baseKey]) {
recordGroups[baseKey] = {}
}
if (record.value.type === 'question') {
recordGroups[baseKey].question = record.value.text
recordGroups[baseKey].user = record.value.author
recordGroups[baseKey].timestamp = timestamp
recordGroups[baseKey].id = record.uri
} else if (record.value.type === 'answer') {
recordGroups[baseKey].answer = record.value.text
recordGroups[baseKey].timestamp = timestamp
}
})
// Convert to history format, only include complete Q&A pairs
const history = Object.values(recordGroups)
.filter(group => group.question && group.answer)
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
.slice(-10) // 最新10件のみ
setChatHistory(history)
logger.log('Chat history loaded:', history.length, 'entries')
} catch (err) {
logError(err, 'useAskAI.loadChatHistory')
// 履歴読み込みエラーは致命的ではない
}
}
return {
askQuestion,
loading,
error,
chatHistory,
clearChatHistory,
loadChatHistory
}
}

View File

@ -0,0 +1,47 @@
import { useState, useEffect } from 'react'
import { OAuthService } from '../services/oauth.js'
const oauthService = new OAuthService()
export function useAuth() {
const [user, setUser] = useState(null)
const [agent, setAgent] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
initAuth()
}, [])
const initAuth = async () => {
try {
const authResult = await oauthService.checkAuth()
if (authResult) {
setUser(authResult.user)
setAgent(authResult.agent)
}
} catch (error) {
console.error('Auth initialization failed:', error)
} finally {
setLoading(false)
}
}
const login = async (handle) => {
await oauthService.login(handle)
}
const logout = async () => {
await oauthService.logout()
setUser(null)
setAgent(null)
}
return {
user,
agent,
loading,
login,
logout,
isAuthenticated: !!user
}
}

View File

@ -0,0 +1,33 @@
import { useState, useEffect } from 'react'
export function usePageContext() {
const [pageContext, setPageContext] = useState({
isTopPage: true,
rkey: null,
url: null
})
useEffect(() => {
const pathname = window.location.pathname
const url = window.location.href
// Extract rkey from URL pattern: /posts/xxx or /posts/xxx.html
const match = pathname.match(/\/posts\/([^/]+)\/?$/)
if (match) {
const rkey = match[1].replace(/\.html$/, '')
setPageContext({
isTopPage: false,
rkey,
url
})
} else {
setPageContext({
isTopPage: true,
rkey: null,
url
})
}
}, [])
return pageContext
}

View File

@ -0,0 +1,169 @@
import { useState, useEffect } from 'react'
import { atproto, collections } from '../api/atproto.js'
import { getApiConfig, isSyuIsHandle } from '../utils/pds.js'
import { env } from '../config/env.js'
export function useUserData(adminData) {
const [userComments, setUserComments] = useState([])
const [chatRecords, setChatRecords] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!adminData?.did || !adminData?.apiConfig) return
const fetchUserData = async () => {
setLoading(true)
setError(null)
try {
// 1. Get user list from admin account
const userListRecords = await collections.getUserList(
adminData.apiConfig.pds,
adminData.did,
env.collection
)
// 2. Get chat records from ai.syui.log.chat
const chatRecords = await collections.getChat(
adminData.apiConfig.pds,
adminData.did,
env.collection
)
setChatRecords(chatRecords)
// 3. Get base collection records which contain user comments
const baseRecords = await collections.getBase(
adminData.apiConfig.pds,
adminData.did,
env.collection
)
// Extract comments from base records
const allUserComments = []
for (const record of baseRecords) {
if (record.value?.comments && Array.isArray(record.value.comments)) {
// Each comment already has author info, so we can use it directly
const commentsWithMeta = record.value.comments.map(comment => ({
uri: record.uri,
cid: record.cid,
value: {
...comment,
post: {
url: record.value.url
}
}
}))
allUserComments.push(...commentsWithMeta)
}
}
// Also try to get individual user records from the user list
// Currently skipping user list processing since users contain placeholder DIDs
if (userListRecords.length > 0 && userListRecords[0].value?.users) {
console.log('User list found, but skipping placeholder users for now')
// Filter out placeholder users
const realUsers = userListRecords[0].value.users.filter(user =>
user.handle &&
user.did &&
!user.did.includes('placeholder') &&
!user.did.includes('example')
)
if (realUsers.length > 0) {
console.log(`Processing ${realUsers.length} real users`)
for (const user of realUsers) {
const userHandle = user.handle
try {
// Get user's DID and PDS using PDS detection logic
let userDid, userPds, userApiConfig
if (user.did && user.pds) {
// Use DID and PDS from user record
userDid = user.did
userPds = user.pds.replace('https://', '')
userApiConfig = getApiConfig(userPds)
} else {
// Auto-detect PDS based on handle and get real DID
if (isSyuIsHandle(userHandle)) {
userPds = env.pds
userApiConfig = getApiConfig(userPds)
userDid = await atproto.getDid(userPds, userHandle)
} else {
userPds = 'bsky.social'
userApiConfig = getApiConfig(userPds)
userDid = await atproto.getDid(userPds, userHandle)
}
}
// Get user's own ai.syui.log records
const userRecords = await collections.getUserComments(
userApiConfig.pds,
userDid,
env.collection
)
// Skip if no records found
if (!userRecords || userRecords.length === 0) {
continue
}
// Get user's profile for enrichment
let profile = null
try {
profile = await atproto.getProfile(userApiConfig.bsky, userDid)
} catch (profileError) {
console.warn(`Failed to get profile for ${userHandle}:`, profileError)
}
// Add profile info to each record
const enrichedRecords = userRecords.map(record => ({
...record,
value: {
...record.value,
author: {
did: userDid,
handle: profile?.data?.handle || userHandle,
displayName: profile?.data?.displayName || userHandle,
avatar: profile?.data?.avatar || null
}
}
}))
allUserComments.push(...enrichedRecords)
} catch (userError) {
console.warn(`Failed to fetch data for user ${userHandle}:`, userError)
}
}
} else {
console.log('No real users found in user list - all appear to be placeholders')
}
}
setUserComments(allUserComments)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchUserData()
}, [adminData])
const refresh = () => {
if (adminData?.did && adminData?.apiConfig) {
// Re-trigger the effect by clearing and re-setting adminData
const currentAdminData = adminData
setUserComments([])
setChatRecords([])
// The useEffect will automatically run again
}
}
return { userComments, chatRecords, loading, error, refresh }
}