import React, { useState, useEffect } from 'react' import { atproto } from './api/atproto.js' import { useAuth } from './hooks/useAuth.js' import { useAdminData } from './hooks/useAdminData.js' import { useUserData } from './hooks/useUserData.js' import { usePageContext } from './hooks/usePageContext.js' import AuthButton from './components/AuthButton.jsx' import RecordTabs from './components/RecordTabs.jsx' import CommentForm from './components/CommentForm.jsx' import ProfileForm from './components/ProfileForm.jsx' import AskAI from './components/AskAI.jsx' import TestUI from './components/TestUI.jsx' import OAuthCallback from './components/OAuthCallback.jsx' export default function App() { const { user, agent, loading: authLoading, login, logout } = useAuth() const { adminData, langRecords, commentRecords, loading: dataLoading, error, retryCount, refresh: refreshAdminData } = useAdminData() const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData) const [userChatRecords, setUserChatRecords] = useState([]) const [userChatLoading, setUserChatLoading] = useState(false) const pageContext = usePageContext() const [showAskAI, setShowAskAI] = useState(false) const [showTestUI, setShowTestUI] = useState(false) // Environment-based feature flags const ENABLE_TEST_UI = import.meta.env.VITE_ENABLE_TEST_UI === 'true' const ENABLE_DEBUG = import.meta.env.VITE_ENABLE_DEBUG === 'true' // Fetch user's own chat records const fetchUserChatRecords = async () => { if (!user || !agent) return setUserChatLoading(true) try { const records = await agent.api.com.atproto.repo.listRecords({ repo: user.did, collection: 'ai.syui.log.chat', limit: 50 }) // Group questions and answers together const chatPairs = [] const recordMap = new Map() // First pass: organize records by base rkey records.data.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 (newest first) chatPairs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) setUserChatRecords(chatPairs) } catch (error) { console.error('Failed to fetch user chat records:', error) setUserChatRecords([]) } finally { setUserChatLoading(false) } } // Fetch user chat records when user/agent changes useEffect(() => { fetchUserChatRecords() }, [user, agent]) // Expose AI profile data to blog's ask-ai.js useEffect(() => { if (adminData?.profile) { console.log('AI profile loaded:', adminData.profile) // Make AI profile data available globally for ask-ai.js window.aiProfileData = { did: adminData.did, handle: adminData.profile.handle, displayName: adminData.profile.displayName, avatar: adminData.profile.avatar } // Dispatch event to notify ask-ai.js window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: window.aiProfileData })) } }, [adminData]) // Event listeners for blog communication useEffect(() => { // Clear OAuth completion flag once app is loaded if (sessionStorage.getItem('oauth_just_completed') === 'true') { setTimeout(() => { sessionStorage.removeItem('oauth_just_completed') }, 1000) } const handleAIQuestion = async (event) => { const { question } = event.detail if (question && adminData && user && agent) { try { console.log('Processing AI question:', question) // AI設定 const aiConfig = { host: import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai', model: import.meta.env.VITE_AI_MODEL || 'gemma3:1b', systemPrompt: import.meta.env.VITE_AI_SYSTEM_PROMPT || 'あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。' } const prompt = `${aiConfig.systemPrompt} Question: ${question} Answer:` // Ollamaに直接リクエスト const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 30000) const response = await fetch(`${aiConfig.host}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Origin': 'https://syui.ai', }, body: JSON.stringify({ model: aiConfig.model, prompt: prompt, stream: false, options: { temperature: 0.9, top_p: 0.9, num_predict: 200, repeat_penalty: 1.1, } }), signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { throw new Error(`Ollama API error: ${response.status}`) } const data = await response.json() const answer = data.response || 'エラーが発生しました' console.log('AI response received:', answer) // Save conversation to ATProto try { const now = new Date() const timestamp = now.toISOString() const rkey = timestamp.replace(/[:.]/g, '-') // Extract post metadata from current page const currentUrl = window.location.href const postSlug = currentUrl.match(/\/posts\/([^/]+)/)?.[1] || '' const postTitle = document.title.replace(' - syui.ai', '') || '' // 1. Save question record const questionRecord = { $type: 'ai.syui.log.chat', post: { url: currentUrl, slug: postSlug, title: postTitle, date: timestamp, tags: [], language: "ja" }, type: "question", text: question, author: { did: user.did, handle: user.handle, displayName: user.displayName || user.handle, avatar: user.avatar }, createdAt: timestamp } await agent.api.com.atproto.repo.putRecord({ repo: user.did, collection: 'ai.syui.log.chat', rkey: rkey, record: questionRecord }) // 2. Save answer record const answerRkey = rkey + '-answer' const answerRecord = { $type: 'ai.syui.log.chat', post: { url: currentUrl, slug: postSlug, title: postTitle, date: timestamp, tags: [], language: "ja" }, type: "answer", text: answer, author: { did: adminData.did, handle: adminData.profile?.handle, displayName: adminData.profile?.displayName, avatar: adminData.profile?.avatar }, createdAt: timestamp } await agent.api.com.atproto.repo.putRecord({ repo: user.did, collection: 'ai.syui.log.chat', rkey: answerRkey, record: answerRecord }) console.log('Question and answer saved to ATProto') // Refresh chat records after saving setTimeout(() => { fetchUserChatRecords() }, 1000) } catch (saveError) { console.error('Failed to save conversation:', saveError) } // Send response to blog window.dispatchEvent(new CustomEvent('aiResponseReceived', { detail: { question: question, answer: answer, timestamp: new Date().toISOString(), aiProfile: adminData?.profile ? { did: adminData.did, handle: adminData.profile.handle, displayName: adminData.profile.displayName, avatar: adminData.profile.avatar } : null } })) } catch (error) { console.error('Failed to process AI question:', error) // Send error response to blog window.dispatchEvent(new CustomEvent('aiResponseReceived', { detail: { question: question, answer: 'エラーが発生しました。もう一度お試しください。', timestamp: new Date().toISOString(), aiProfile: adminData?.profile ? { did: adminData.did, handle: adminData.profile.handle, displayName: adminData.profile.displayName, avatar: adminData.profile.avatar } : null } })) } } } const dispatchAIProfileLoaded = () => { if (adminData?.profile) { window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: { did: adminData.did, handle: adminData.profile.handle, displayName: adminData.profile.displayName, avatar: adminData.profile.avatar } })) } } // Listen for questions from blog window.addEventListener('postAIQuestion', handleAIQuestion) // Dispatch AI profile when adminData is available if (adminData?.profile) { dispatchAIProfileLoaded() } return () => { window.removeEventListener('postAIQuestion', handleAIQuestion) } }, [adminData, user, agent]) // Handle OAuth callback if (window.location.search.includes('code=')) { return } const isLoading = authLoading || dataLoading || userLoading // Don't show loading if we just completed OAuth callback const isOAuthReturn = window.location.pathname === '/oauth/callback' || sessionStorage.getItem('oauth_just_completed') === 'true' if (isLoading && !isOAuthReturn) { return (

読み込み中...

) } if (error) { return (

エラー

エラー: {error}

{retryCount > 0 && (

自動リトライ中... ({retryCount}/3)

)}
) } return (
{user && (
{user.avatar ? ( {user.displayName ) : (
{(user.displayName || user.handle || '?').charAt(0).toUpperCase()}
)}
{user.displayName || user.handle}
@{user.handle}
{user.did}
)}
{user && (
{ refreshAdminData?.() refreshUserData?.() }} />
)} {user && (
{ refreshAdminData?.() refreshUserData?.() }} />
)} { refreshAdminData?.() refreshUserData?.() fetchUserChatRecords?.() }} /> {ENABLE_TEST_UI && showTestUI && (
)} {ENABLE_TEST_UI && (
)}
) }