fix mobile css, ask-ai

This commit is contained in:
2025-06-19 17:27:16 +09:00
parent 8541af9293
commit f816abb84f
10 changed files with 392 additions and 59 deletions

View File

@ -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;
}

View File

@ -1,3 +1,3 @@
<!-- OAuth Comment System - Load globally for session management -->
<script type="module" crossorigin src="/assets/comment-atproto-BQKPMV57.js"></script>
<script type="module" crossorigin src="/assets/comment-atproto-D0RrISz4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-BUFiApUA.css">

View File

@ -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;

View File

@ -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 (
<div style={{
display: 'flex',

View File

@ -65,7 +65,7 @@ export const atproto = {
async getProfile(bsky, actor) {
// Skip test DIDs
if (actor && actor.includes('test-')) {
logger.log('Skipping profile fetch for test DID:', actor)
console.log('Skipping profile fetch for test DID:', actor)
return {
did: actor,
handle: 'test.user',
@ -74,7 +74,17 @@ export const atproto = {
}
}
return await request(`${bsky}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
// Check if endpoint supports getProfile
let apiEndpoint = bsky
// Allow public.api.bsky.app and bsky.syu.is, redirect other PDS endpoints
if (!bsky.includes('public.api.bsky.app') && !bsky.includes('bsky.syu.is')) {
// If it's a PDS endpoint that doesn't support getProfile, redirect to public API
console.warn(`getProfile called with PDS endpoint ${bsky}, redirecting to public API`)
apiEndpoint = 'https://public.api.bsky.app'
}
return await request(`${apiEndpoint}/xrpc/${ENDPOINTS.getProfile}?actor=${actor}`)
},
async getRecords(pds, repo, collection, limit = 10) {

View File

@ -2,6 +2,24 @@ import React, { useState } from 'react'
import AvatarImage from './AvatarImage.jsx'
import Avatar from './Avatar.jsx'
// Helper function to get correct web URL based on avatar URL
function getCorrectWebUrl(avatarUrl) {
if (!avatarUrl) return 'https://bsky.app'
// If avatar is from bsky.app (main Bluesky), use bsky.app
if (avatarUrl.includes('cdn.bsky.app') || avatarUrl.includes('bsky.app')) {
return 'https://bsky.app'
}
// If avatar is from syu.is, use web.syu.is
if (avatarUrl.includes('bsky.syu.is') || avatarUrl.includes('syu.is')) {
return 'https://web.syu.is'
}
// Default to bsky.app
return 'https://bsky.app'
}
export default function RecordList({ title, records, apiConfig, showTitle = true, user = null, agent = null, onRecordDeleted = null }) {
const [expandedRecords, setExpandedRecords] = useState(new Set())
const [deletingRecords, setDeletingRecords] = useState(new Set())
@ -74,7 +92,7 @@ export default function RecordList({ title, records, apiConfig, showTitle = true
<div className="display-name">{record.value.author?.displayName || record.value.author?.handle}</div>
<div className="handle">
<a
href={`${apiConfig?.web || 'https://bsky.app'}/profile/${record.value.author?.did}`}
href={`${getCorrectWebUrl(record.value.author?.avatar)}/profile/${record.value.author?.did}`}
target="_blank"
rel="noopener noreferrer"
className="handle-link"

View File

@ -38,6 +38,8 @@ export function useAuth() {
user: authResult.user
}, '*')
} else {
// Set flag to skip loading screen after redirect
sessionStorage.setItem('oauth_just_completed', 'true')
// Direct redirect
setTimeout(() => {
window.location.href = returnUrl

View File

@ -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)
}
}

View File

@ -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}`,

View File

@ -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`