diff --git a/my-blog/static/css/style.css b/my-blog/static/css/style.css index 57ebe96..972f0b6 100644 --- a/my-blog/static/css/style.css +++ b/my-blog/static/css/style.css @@ -664,7 +664,8 @@ article.article-content { .chat-message.comment-style { background: #ffffff; border: 1px solid #d1d9e0; - border-radius: 8px; + border-left: 4px solid var(--theme-color); + border-radius: 8px; padding: 16px; margin-bottom: 12px; } diff --git a/my-blog/static/oauth/index.html b/my-blog/static/oauth/index.html index ee853ad..7380ee8 100644 --- a/my-blog/static/oauth/index.html +++ b/my-blog/static/oauth/index.html @@ -1,3 +1,3 @@ - + diff --git a/oauth/src/App.css b/oauth/src/App.css index e056258..4c96588 100644 --- a/oauth/src/App.css +++ b/oauth/src/App.css @@ -43,9 +43,9 @@ body { } .oauth-header-content { - display: flex; - justify-content: center; - align-items: center; + /* display: flex; */ + /* justify-content: center; */ + /* align-items: center; */ max-width: 800px; margin: 0 auto; padding: 20px 0; @@ -205,7 +205,7 @@ body { padding: 0; gap: 0; width: 100%; - max-width: 400px; + /* max-width: 400px; */ } .auth-section.search-bar-layout .handle-input { @@ -683,79 +683,250 @@ body { } /* Responsive */ -@media (max-width: 768px) { +@media (max-width: 1000px) { + /* Global mobile constraints */ + * { + max-width: 100% !important; + box-sizing: border-box !important; + } + + body { + overflow-x: hidden !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + .app { + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; + padding: 0 !important; + margin: 0 !important; + } + .main-content { - max-width: 100%; + padding: 0px !important; + margin: 0px !important; + max-width: 100% !important; + width: 100% !important; + overflow-x: hidden !important; + } + + /* OAuth app mobile fixes - prevent overflow and content issues */ + .oauth-app-header { + padding: 0px !important; + margin: 0px !important; + border: none !important; + width: 100% !important; + max-width: 100% !important; + } + + .oauth-header-content { + max-width: 100% !important; + width: 100% !important; + padding: 10px 15px !important; + margin: 0px !important; + overflow-x: hidden !important; + } + + .oauth-header-actions { + width: auto !important; + max-width: 100% !important; + overflow: hidden !important; } .content-area { - border-left: none; - border-right: none; + padding: 0px !important; + margin: 0px !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; } .card { - margin: 0; - border-radius: 0; - border-left: none; - border-right: none; + margin: 0px !important; + border-radius: 0px !important; + border-left: none !important; + border-right: none !important; + max-width: 100% !important; } - .app-header { - padding: 8px 16px; + .card-content { + padding: 15px !important; } - .header-actions { - gap: 4px; - } - - .btn { - padding: 6px 12px; - font-size: 14px; - } - - .tab-btn { - padding: 12px 16px; - font-size: 14px; + .comment-form { + padding: 15px !important; } .record-item { - padding: 12px 16px; + padding: 15px !important; + margin: 0px !important; + border-left: none !important; + border-right: none !important; + } + + .record-content { + word-wrap: break-word !important; + overflow-wrap: break-word !important; + max-width: 100% !important; + } + + .record-meta { + word-break: break-all !important; + overflow-wrap: break-word !important; + flex-wrap: wrap !important; + } + + .record-url { + word-break: break-all !important; + max-width: 100% !important; + } + + .form-group { + margin-bottom: 15px !important; + } + + .form-input, .form-textarea { + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + padding: 12px !important; + } + + .auth-section { + padding: 0px !important; + max-width: 100% !important; + overflow: hidden !important; + } + + .auth-section.search-bar-layout { + width: 100% !important; + max-width: 100% !important; + } + + .auth-section.search-bar-layout .handle-input { + max-width: calc(100% - 80px) !important; + width: calc(100% - 80px) !important; + } + + .auth-button { + white-space: nowrap !important; + min-width: 70px !important; + } + + .tab-header { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + width: 100% !important; + display: flex !important; + scrollbar-width: none !important; /* Firefox */ + -ms-overflow-style: none !important; /* IE/Edge */ + } + + .tab-header::-webkit-scrollbar { + display: none !important; /* Chrome/Safari */ + } + + .tab-btn { + white-space: nowrap !important; + min-width: auto !important; + padding: 12px 16px !important; + flex-shrink: 0 !important; + font-size: 13px !important; + } + + .json-content { + font-size: 10px !important; + padding: 8px !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + } + + .ask-ai-container { + margin: 0px !important; + border-radius: 0px !important; + border-left: none !important; + border-right: none !important; } .chat-container { - height: 300px; + height: 250px !important; + padding: 12px !important; } - /* OAuth User Profile Mobile */ + .question-form { + padding: 12px !important; + } + + .input-container { + flex-direction: column !important; + gap: 12px !important; + } + + .question-input { + width: 100% !important; + box-sizing: border-box !important; + } + + .send-btn { + width: 100% !important; + height: 44px !important; + } + .oauth-user-profile { gap: 8px; + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; } - + + .profile-info { + flex: 1 !important; + min-width: 0 !important; + max-width: calc(100% - 50px) !important; + overflow: hidden !important; + } + .profile-avatar-section .profile-avatar, .profile-avatar-fallback { width: 36px; height: 36px; font-size: 14px; + flex-shrink: 0 !important; } - + .profile-display-name { font-size: 14px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; } - + .profile-handle { font-size: 12px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; } - + .profile-did { font-size: 9px; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 100% !important; } - + .oauth-header-content { flex-direction: column; gap: 12px; - align-items: flex-start; + /* align-items: flex-start; */ } - + .oauth-header-actions { width: 100%; justify-content: center; diff --git a/oauth/src/App.jsx b/oauth/src/App.jsx index 11fac13..e597cbf 100644 --- a/oauth/src/App.jsx +++ b/oauth/src/App.jsx @@ -24,13 +24,130 @@ export default function App() { // Event listeners for blog communication useEffect(() => { - const handleAIQuestion = (event) => { + // 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) { - // Automatically open Ask AI panel and submit question - setShowAskAI(true) - // We'll need to pass this to the AskAI component - // For now, let's just open the panel + 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 timestamp = new Date().toISOString() + const conversationRecord = { + repo: adminData.did, + collection: 'ai.syui.log.chat', + record: { + type: 'ai.syui.log.chat', + question: question, + answer: answer, + user: user ? { + did: user.did, + handle: user.handle, + displayName: user.displayName || user.handle + } : null, + ai: { + did: adminData.did, + handle: adminData.profile?.handle, + displayName: adminData.profile?.displayName + }, + timestamp: timestamp, + createdAt: timestamp + } + } + + await agent.com.atproto.repo.putRecord(conversationRecord) + console.log('Conversation saved to ATProto') + } 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 + } + })) + } } } @@ -67,7 +184,11 @@ export default function App() { const isLoading = authLoading || dataLoading || userLoading - if (isLoading) { + // 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 (
{record.value.author?.displayName || record.value.author?.handle}
{ window.location.href = returnUrl diff --git a/oauth/src/hooks/useUserData.js b/oauth/src/hooks/useUserData.js index ea0bc37..fd8c313 100644 --- a/oauth/src/hooks/useUserData.js +++ b/oauth/src/hooks/useUserData.js @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { atproto, collections } from '../api/atproto.js' -import { getApiConfig, isSyuIsHandle } from '../utils/pds.js' +import { getApiConfig, isSyuIsHandle, getPdsFromHandle } from '../utils/pds.js' import { env } from '../config/env.js' export function useUserData(adminData) { @@ -88,14 +88,21 @@ export function useUserData(adminData) { userPds = user.pds.replace('https://', '') userApiConfig = getApiConfig(userPds) } else { - // Auto-detect PDS based on handle and get real DID - if (isSyuIsHandle(userHandle)) { + // Always get actual PDS from describeRepo first + try { + // Try bsky.social first for most handles + const bskyPds = 'bsky.social' + userDid = await atproto.getDid(bskyPds, userHandle) + + // Get the actual PDS endpoint from DID + const realPds = await getPdsFromHandle(userHandle) + userPds = realPds.replace('https://', '') + userApiConfig = getApiConfig(realPds) + } catch (error) { + // Fallback to syu.is if bsky.social fails + console.warn(`Failed to get PDS for ${userHandle} from bsky.social, trying syu.is:`, error) userPds = env.pds - userApiConfig = getApiConfig(userPds) - userDid = await atproto.getDid(userPds, userHandle) - } else { - userPds = 'bsky.social' - userApiConfig = getApiConfig(userPds) + userApiConfig = getApiConfig(env.pds) userDid = await atproto.getDid(userPds, userHandle) } } diff --git a/oauth/src/utils/pds.js b/oauth/src/utils/pds.js index da23aa4..88f0122 100644 --- a/oauth/src/utils/pds.js +++ b/oauth/src/utils/pds.js @@ -1,8 +1,11 @@ import { env } from '../config/env.js' -// PDS判定からAPI設定を取得 +// PDS判定からAPI設定を取得 - 実際のPDSエンドポイントに基づいて設定 export function getApiConfig(pds) { - if (pds.includes(env.pds)) { + // pdsからhttps://を除去してドメインのみ取得 + const cleanPds = pds.replace(/^https?:\/\//, '') + + if (cleanPds.includes(env.pds)) { return { pds: `https://${env.pds}`, bsky: `https://bsky.${env.pds}`, diff --git a/scpt/delete-chat-records.zsh b/scpt/delete-chat-records.zsh index ae8d297..572eee8 100755 --- a/scpt/delete-chat-records.zsh +++ b/scpt/delete-chat-records.zsh @@ -3,11 +3,11 @@ set -e cb=ai.syui.log -cl=( $cb.chat.lang $cb.chat.comment) +cl=($cb.chat $cb.user $cb ) f=~/.config/syui/ai/log/config.json default_collection="ai.syui.log.chat" -default_pds="syu.is" +default_pds=bsky.social default_did=`cat $f|jq -r .admin.did` default_token=`cat $f|jq -r .admin.access_jwt` default_refresh=`cat $f|jq -r .admin.refresh_jwt`