fix hugo callback
This commit is contained in:
@ -44,7 +44,7 @@ body {
|
||||
|
||||
.oauth-header-content {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
@ -52,6 +52,10 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oauth-header-content:has(.oauth-user-profile) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.oauth-app-title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
@ -62,7 +66,80 @@ body {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* When user is logged in, actions take normal space */
|
||||
.oauth-header-content:has(.oauth-user-profile) .oauth-header-actions {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* OAuth User Profile in Header */
|
||||
.oauth-user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profile-avatar-section {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-avatar-section .profile-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.profile-avatar-fallback {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--background-secondary);
|
||||
border: 2px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-display-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-handle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-did {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
@ -128,6 +205,7 @@ body {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .handle-input {
|
||||
@ -465,7 +543,7 @@ body {
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
color: white;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.question-form {
|
||||
@ -647,6 +725,41 @@ body {
|
||||
.chat-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* OAuth User Profile Mobile */
|
||||
.oauth-user-profile {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profile-avatar-section .profile-avatar,
|
||||
.profile-avatar-fallback {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.profile-display-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.profile-handle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.profile-did {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.oauth-header-content {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.oauth-header-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Avatar Styles */
|
||||
|
@ -69,8 +69,32 @@ export default function App() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<p>読み込み中...</p>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
padding: '40px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
border: '4px solid #f3f3f3',
|
||||
borderTop: '4px solid #667eea',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginBottom: '16px'
|
||||
}} />
|
||||
<p style={{ color: '#666', margin: 0 }}>読み込み中...</p>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -115,6 +139,34 @@ export default function App() {
|
||||
<div className="app">
|
||||
<header className="oauth-app-header">
|
||||
<div className="oauth-header-content">
|
||||
{user && (
|
||||
<div className="oauth-user-profile">
|
||||
<div className="profile-avatar-section">
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.displayName || user.handle}
|
||||
className="profile-avatar"
|
||||
/>
|
||||
) : (
|
||||
<div className="profile-avatar-fallback">
|
||||
{(user.displayName || user.handle || '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="profile-info">
|
||||
<div className="profile-display-name">
|
||||
{user.displayName || user.handle}
|
||||
</div>
|
||||
<div className="profile-handle">
|
||||
@{user.handle}
|
||||
</div>
|
||||
<div className="profile-did">
|
||||
{user.did}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="oauth-header-actions">
|
||||
<AuthButton
|
||||
user={user}
|
||||
|
@ -63,6 +63,17 @@ export const atproto = {
|
||||
},
|
||||
|
||||
async getProfile(bsky, actor) {
|
||||
// Skip test DIDs
|
||||
if (actor && actor.includes('test-')) {
|
||||
logger.log('Skipping profile fetch for test DID:', actor)
|
||||
return {
|
||||
did: actor,
|
||||
handle: 'test.user',
|
||||
displayName: 'Test User',
|
||||
avatar: null
|
||||
}
|
||||
}
|
||||
|
||||
return await request(`${bsky}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
|
||||
},
|
||||
|
||||
|
@ -1,54 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export default function OAuthCallback({ onAuthSuccess }) {
|
||||
const [status, setStatus] = useState('OAuth認証処理中...')
|
||||
|
||||
useEffect(() => {
|
||||
handleCallback()
|
||||
}, [])
|
||||
|
||||
const handleCallback = async () => {
|
||||
try {
|
||||
// BrowserOAuthClientが自動的にコールバックを処理します
|
||||
// URLのパラメータを確認して成功を通知
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
const error = urlParams.get('error')
|
||||
|
||||
if (error) {
|
||||
throw new Error(`OAuth error: ${error}`)
|
||||
}
|
||||
|
||||
if (code) {
|
||||
setStatus('認証成功!元のページに戻ります...')
|
||||
|
||||
// Get the referring page or use root
|
||||
const referrer = document.referrer || window.location.origin
|
||||
const returnUrl = referrer.includes('/oauth/callback') ? window.location.origin : referrer
|
||||
|
||||
// 少し待ってから元のページにリダイレクト
|
||||
setTimeout(() => {
|
||||
window.location.href = returnUrl
|
||||
}, 1500)
|
||||
} else {
|
||||
setStatus('認証情報が見つかりません')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Callback error:', error)
|
||||
setStatus('認証エラー: ' + error.message)
|
||||
}
|
||||
}
|
||||
import React from 'react'
|
||||
|
||||
export default function OAuthCallback() {
|
||||
return (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<h2>OAuth認証</h2>
|
||||
<p>{status}</p>
|
||||
{status.includes('エラー') && (
|
||||
<button onClick={() => window.location.href = '/'}>
|
||||
メインページに戻る
|
||||
</button>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '50vh',
|
||||
padding: '40px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
border: '4px solid #f3f3f3',
|
||||
borderTop: '4px solid #667eea',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginBottom: '20px'
|
||||
}} />
|
||||
<h2 style={{ color: '#333', marginBottom: '12px' }}>OAuth認証処理中...</h2>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>
|
||||
認証が完了しましたら自動で元のページに戻ります
|
||||
</p>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -9,8 +9,12 @@ export function useAskAI(adminData, userProfile, agent) {
|
||||
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'
|
||||
// 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 askQuestion = async (question) => {
|
||||
if (!question.trim()) return
|
||||
@ -19,28 +23,47 @@ export function useAskAI(adminData, userProfile, agent) {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
logger.log('Sending question to ask-AI:', question)
|
||||
logger.log('Sending question to Ollama:', question)
|
||||
|
||||
// ask-AIサーバーにリクエスト送信
|
||||
const response = await fetch(askAIUrl, {
|
||||
// Ollamaに直接リクエスト送信(oauth_oldと同じ方式)
|
||||
const prompt = `${aiConfig.systemPrompt}
|
||||
|
||||
Question: ${question}
|
||||
|
||||
Answer:`
|
||||
|
||||
// Add timeout to fetch request
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
|
||||
|
||||
const response = await fetch(`${aiConfig.host}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Origin': 'https://syui.ai',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question: question.trim(),
|
||||
context: {
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString()
|
||||
model: aiConfig.model,
|
||||
prompt: prompt,
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: 0.9,
|
||||
top_p: 0.9,
|
||||
num_predict: 200, // Longer responses for better answers
|
||||
repeat_penalty: 1.1,
|
||||
}
|
||||
})
|
||||
}),
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`ask-AI server error: ${response.status}`)
|
||||
throw new Error(`Ollama API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const aiResponse = await response.json()
|
||||
const data = await response.json()
|
||||
const aiResponse = { answer: data.response || 'エラーが発生しました' }
|
||||
logger.log('Received AI response:', aiResponse)
|
||||
|
||||
// AI回答をチャット履歴に追加
|
||||
@ -81,7 +104,17 @@ export function useAskAI(adminData, userProfile, agent) {
|
||||
|
||||
} catch (err) {
|
||||
logError(err, 'useAskAI.askQuestion')
|
||||
setError(getErrorMessage(err))
|
||||
|
||||
let errorMessage = 'AI応答の生成に失敗しました'
|
||||
if (err.name === 'AbortError') {
|
||||
errorMessage = 'AI応答がタイムアウトしました(30秒)'
|
||||
} else if (err.message.includes('Ollama API error')) {
|
||||
errorMessage = `Ollama API エラー: ${err.message}`
|
||||
} else if (err.message.includes('Failed to fetch')) {
|
||||
errorMessage = 'AI サーバーに接続できませんでした'
|
||||
}
|
||||
|
||||
setError(errorMessage)
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
@ -18,6 +18,32 @@ export function useAuth() {
|
||||
if (authResult) {
|
||||
setUser(authResult.user)
|
||||
setAgent(authResult.agent)
|
||||
|
||||
// If we're on callback page and authentication succeeded, notify parent
|
||||
if (window.location.pathname === '/oauth/callback') {
|
||||
console.log('OAuth callback completed, notifying parent window')
|
||||
|
||||
// Get referrer or use stored return URL
|
||||
const returnUrl = sessionStorage.getItem('oauth_return_url') ||
|
||||
document.referrer ||
|
||||
window.location.origin
|
||||
|
||||
sessionStorage.removeItem('oauth_return_url')
|
||||
|
||||
// Notify parent window if in iframe, otherwise redirect directly
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'oauth_success',
|
||||
returnUrl: returnUrl,
|
||||
user: authResult.user
|
||||
}, '*')
|
||||
} else {
|
||||
// Direct redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = returnUrl
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization failed:', error)
|
||||
@ -27,6 +53,11 @@ export function useAuth() {
|
||||
}
|
||||
|
||||
const login = async (handle) => {
|
||||
// Store current page URL for post-auth redirect
|
||||
if (window.location.pathname !== '/oauth/callback') {
|
||||
sessionStorage.setItem('oauth_return_url', window.location.href)
|
||||
}
|
||||
|
||||
await oauthService.login(handle)
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,8 @@ export class OAuthService {
|
||||
async processSession(session) {
|
||||
const did = session.sub || session.did
|
||||
let handle = session.handle || 'unknown'
|
||||
let displayName = null
|
||||
let avatar = null
|
||||
|
||||
// Create Agent directly with session (per official docs)
|
||||
try {
|
||||
@ -77,21 +79,43 @@ export class OAuthService {
|
||||
})
|
||||
}
|
||||
|
||||
this.sessionInfo = { did, handle }
|
||||
|
||||
// Resolve handle if missing
|
||||
if (handle === 'unknown' && this.agent) {
|
||||
// Get profile information using authenticated agent
|
||||
// Skip test DIDs
|
||||
if (this.agent && did && !did.includes('test-')) {
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
const profile = await this.agent.getProfile({ actor: did })
|
||||
handle = profile.data.handle
|
||||
this.sessionInfo.handle = handle
|
||||
handle = profile.data.handle || handle
|
||||
displayName = profile.data.displayName || null
|
||||
avatar = profile.data.avatar || null
|
||||
|
||||
console.log('Profile fetched from session:', {
|
||||
did,
|
||||
handle,
|
||||
displayName,
|
||||
avatar: avatar ? 'present' : 'none'
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('Failed to resolve handle:', error)
|
||||
console.log('Failed to get profile from session:', error)
|
||||
// Keep the basic info we have
|
||||
}
|
||||
} else if (did && did.includes('test-')) {
|
||||
console.log('Skipping profile fetch for test DID:', did)
|
||||
}
|
||||
|
||||
return { did, handle }
|
||||
this.sessionInfo = {
|
||||
did,
|
||||
handle,
|
||||
displayName,
|
||||
avatar
|
||||
}
|
||||
|
||||
return {
|
||||
did,
|
||||
handle,
|
||||
displayName,
|
||||
avatar
|
||||
}
|
||||
}
|
||||
|
||||
async login(handle) {
|
||||
|
@ -33,6 +33,12 @@ async function getDid(handle) {
|
||||
|
||||
// DIDからプロフィール情報を取得
|
||||
async function getProfile(did, handle) {
|
||||
// Skip test DIDs
|
||||
if (did && did.includes('test-')) {
|
||||
logger.log('Skipping profile fetch for test DID:', did)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine which public API to use based on handle
|
||||
const pds = await getPdsFromHandle(handle)
|
||||
@ -81,6 +87,11 @@ async function fetchFreshAvatar(handle, did) {
|
||||
|
||||
// プロフィール取得
|
||||
const profile = await getProfile(actualDid, handle)
|
||||
if (!profile) {
|
||||
// Test DID or profile fetch failed
|
||||
return null
|
||||
}
|
||||
|
||||
const avatarUrl = profile.avatar || null
|
||||
|
||||
// キャッシュに保存
|
||||
|
Reference in New Issue
Block a user