diff --git a/my-blog/static/css/style.css b/my-blog/static/css/style.css
index d0a80d0..fbb2138 100644
--- a/my-blog/static/css/style.css
+++ b/my-blog/static/css/style.css
@@ -200,7 +200,7 @@ a.view-markdown:any-link {
/* Main Content */
.main-content {
grid-area: main;
- max-width: 1000px;
+ max-width: 800px;
margin: 0 auto;
padding: 0px;
width: 100%;
@@ -969,6 +969,41 @@ article.article-content {
.question-form {
padding: 12px !important;
}
+}
+
+/* Profile Display Styles */
+.profile-avatar-fallback {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: var(--theme-color);
+ color: var(--white);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: 700;
+}
+
+.admin-badge {
+ background: var(--theme-color);
+ color: var(--white);
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-weight: 500;
+ margin-left: 8px;
+}
+
+.loading-message, .error-message, .no-profiles {
+ text-align: center;
+ padding: 20px;
+ color: var(--dark-gray);
+}
+
+.error-message {
+ color: #d32f2f;
+}
.input-container {
flex-direction: column !important;
diff --git a/my-blog/static/js/ask-ai.js b/my-blog/static/js/ask-ai.js
index 824a3b4..2dbebae 100644
--- a/my-blog/static/js/ask-ai.js
+++ b/my-blog/static/js/ask-ai.js
@@ -37,10 +37,74 @@ function checkAuthenticationStatus() {
document.getElementById('aiQuestion').focus();
}, 50);
} else {
- // User not authenticated - show auth message
- document.getElementById('authCheck').style.display = 'block';
+ // User not authenticated - show profiles instead of auth message
+ document.getElementById('authCheck').style.display = 'none';
document.getElementById('chatForm').style.display = 'none';
- document.getElementById('chatHistory').style.display = 'none';
+ document.getElementById('chatHistory').style.display = 'block';
+ loadAndShowProfiles();
+ }
+}
+
+// Load and display profiles from ai.syui.log.profile collection
+async function loadAndShowProfiles() {
+ const chatHistory = document.getElementById('chatHistory');
+ chatHistory.innerHTML = '
Loading profiles...
';
+
+ try {
+ const ADMIN_HANDLE = 'ai.syui.ai';
+ const OAUTH_COLLECTION = 'ai.syui.log';
+ const ATPROTO_PDS = 'syu.is';
+
+ const response = await fetch(`https://${ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${ADMIN_HANDLE}&collection=${OAUTH_COLLECTION}.profile&limit=100`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch profiles');
+ }
+
+ const data = await response.json();
+ const profiles = (data.records || []).sort((a, b) => {
+ if (a.value.type === 'admin' && b.value.type !== 'admin') return -1;
+ if (a.value.type !== 'admin' && b.value.type === 'admin') return 1;
+ return 0;
+ });
+
+ // Clear loading message
+ chatHistory.innerHTML = '';
+
+ // Display profiles using the same format as chat
+ profiles.forEach(profile => {
+ const profileDiv = document.createElement('div');
+ profileDiv.className = 'chat-message ai-message comment-style';
+
+ const avatarElement = profile.value.author.avatar
+ ? `
`
+ : `${(profile.value.author.displayName || profile.value.author.handle || '?').charAt(0).toUpperCase()}
`;
+
+ const adminBadge = profile.value.type === 'admin'
+ ? 'Admin'
+ : '';
+
+ profileDiv.innerHTML = `
+
+ ${profile.value.text}
+ `;
+ chatHistory.appendChild(profileDiv);
+ });
+
+ if (profiles.length === 0) {
+ chatHistory.innerHTML = 'No profiles available
';
+ }
+
+ } catch (error) {
+ console.error('Error loading profiles:', error);
+ chatHistory.innerHTML = 'Failed to load profiles. Please try again later.
';
}
}
diff --git a/oauth/src/App.css b/oauth/src/App.css
index 03439b4..f403a2f 100644
--- a/oauth/src/App.css
+++ b/oauth/src/App.css
@@ -34,6 +34,96 @@ body {
background: var(--background);
}
+/* Profile Form Styles */
+.profile-form-container {
+ background: var(--background-secondary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.profile-form-container h3 {
+ margin: 0 0 16px 0;
+ color: var(--text);
+}
+
+.profile-form .form-row {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.profile-form .form-group {
+ flex: 1;
+}
+
+.profile-form .form-group label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.profile-form .form-group input,
+.profile-form .form-group select,
+.profile-form .form-group textarea {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-size: 14px;
+ transition: border-color 0.2s;
+}
+
+.profile-form .form-group input:focus,
+.profile-form .form-group select:focus,
+.profile-form .form-group textarea:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.profile-form .form-group textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+.profile-form .submit-btn {
+ background: var(--primary);
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background-color 0.2s;
+}
+
+.profile-form .submit-btn:hover:not(:disabled) {
+ background: var(--primary-hover);
+}
+
+.profile-form .submit-btn:disabled {
+ background: var(--text-secondary);
+ cursor: not-allowed;
+}
+
+/* Profile Record List Styles */
+.profile-record-list .record-item.admin {
+ border-left: 4px solid var(--primary);
+}
+
+.profile-record-list .admin-badge {
+ background: var(--primary);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-weight: 500;
+ margin-left: 8px;
+}
+
/* Header */
.oauth-app-header {
background: var(--background);
@@ -264,7 +354,7 @@ body {
.main-content {
grid-area: main;
- max-width: 1000px;
+ max-width: 800px;
margin: 0 auto;
padding: 0px;
width: 100%;
diff --git a/oauth/src/App.jsx b/oauth/src/App.jsx
index b684ea5..490a4b8 100644
--- a/oauth/src/App.jsx
+++ b/oauth/src/App.jsx
@@ -7,6 +7,7 @@ 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'
@@ -428,6 +429,20 @@ Answer:`
)}
+ {user && (
+
+
{
+ refreshAdminData?.()
+ refreshUserData?.()
+ }}
+ />
+
+ )}
+
{
+ const [text, setText] = useState('')
+ const [type, setType] = useState('user')
+ const [handle, setHandle] = useState('')
+ const [rkey, setRkey] = useState('')
+ const [posting, setPosting] = useState(false)
+ const [error, setError] = useState('')
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!text.trim() || !handle.trim() || !rkey.trim()) {
+ setError('すべてのフィールドを入力してください')
+ return
+ }
+
+ setPosting(true)
+ setError('')
+
+ try {
+ // Get handle information
+ let authorData
+ try {
+ const handleDid = await atproto.getDid(apiConfig.pds, handle)
+ authorData = await atproto.getProfile(apiConfig.bsky, handleDid)
+ } catch (err) {
+ throw new Error('ハンドルが見つかりません')
+ }
+
+ // Create record
+ const record = {
+ repo: user.did,
+ collection: `${apiConfig.collection}.profile`,
+ rkey: rkey,
+ record: {
+ $type: `${apiConfig.collection}.profile`,
+ text: text,
+ type: type,
+ author: {
+ did: authorData.did,
+ handle: authorData.handle,
+ displayName: authorData.displayName || authorData.handle,
+ avatar: authorData.avatar || null
+ },
+ createdAt: new Date().toISOString(),
+ post: {
+ url: window.location.origin,
+ date: new Date().toISOString(),
+ slug: '',
+ tags: [],
+ title: 'Profile',
+ language: 'ja'
+ }
+ }
+ }
+
+ await atproto.putRecord(apiConfig.pds, record, agent)
+
+ // Invalidate cache and refresh
+ collections.invalidateCache(`${apiConfig.collection}.profile`)
+
+ // Reset form
+ setText('')
+ setType('user')
+ setHandle('')
+ setRkey('')
+
+ if (onProfilePosted) {
+ onProfilePosted()
+ }
+
+ } catch (err) {
+ console.error('Failed to create profile:', err)
+ setError(err.message || 'プロフィールの作成に失敗しました')
+ } finally {
+ setPosting(false)
+ }
+ }
+
+ if (!user) {
+ return null
+ }
+
+ return (
+
+
プロフィール投稿
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ )
+}
+
+export default ProfileForm
\ No newline at end of file
diff --git a/oauth/src/components/ProfileRecordList.jsx b/oauth/src/components/ProfileRecordList.jsx
new file mode 100644
index 0000000..a26be28
--- /dev/null
+++ b/oauth/src/components/ProfileRecordList.jsx
@@ -0,0 +1,133 @@
+import React, { useState, useEffect } from 'react'
+import { collections } from '../api/atproto.js'
+import AvatarImage from './AvatarImage.jsx'
+import LoadingSkeleton from './LoadingSkeleton.jsx'
+
+const ProfileRecordList = ({ apiConfig, user, agent, onRecordDeleted }) => {
+ const [profiles, setProfiles] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (apiConfig?.admin && apiConfig?.collection) {
+ fetchProfiles()
+ }
+ }, [apiConfig])
+
+ const fetchProfiles = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+
+ const adminProfiles = await collections.getProfiles(
+ apiConfig.pds,
+ apiConfig.admin,
+ apiConfig.collection
+ )
+
+ // Sort profiles: admin type first, then user type
+ const sortedProfiles = adminProfiles.sort((a, b) => {
+ if (a.value.type === 'admin' && b.value.type !== 'admin') return -1
+ if (a.value.type !== 'admin' && b.value.type === 'admin') return 1
+ return 0
+ })
+
+ setProfiles(sortedProfiles)
+ } catch (err) {
+ console.error('Failed to fetch profiles:', err)
+ setError('プロフィールの読み込みに失敗しました')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleDelete = async (uri) => {
+ if (!user || !agent) return
+ if (!confirm('このプロフィールを削除しますか?')) return
+
+ try {
+ const rkey = uri.split('/').pop()
+ await agent.api.com.atproto.repo.deleteRecord({
+ repo: user.did,
+ collection: `${apiConfig.collection}.profile`,
+ rkey: rkey
+ })
+
+ // Invalidate cache and refresh
+ collections.invalidateCache(`${apiConfig.collection}.profile`)
+ await fetchProfiles()
+
+ if (onRecordDeleted) {
+ onRecordDeleted()
+ }
+ } catch (err) {
+ console.error('Failed to delete profile:', err)
+ setError('プロフィールの削除に失敗しました')
+ }
+ }
+
+ if (loading) {
+ return
+ }
+
+ if (error) {
+ return (
+
+ )
+ }
+
+ if (profiles.length === 0) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {profiles.map((profile) => (
+
+
+
+
+
+ {profile.value.author.displayName || profile.value.author.handle}
+ {profile.value.type === 'admin' && (
+ Admin
+ )}
+
+
@{profile.value.author.handle}
+
+ {new Date(profile.value.createdAt).toLocaleString()}
+
+
+ {user && (
+
+
+
+ )}
+
+
{profile.value.text}
+
+ ))}
+
+ )
+}
+
+export default ProfileRecordList
\ No newline at end of file
diff --git a/oauth/src/components/RecordTabs.jsx b/oauth/src/components/RecordTabs.jsx
index 514d371..c7f139e 100644
--- a/oauth/src/components/RecordTabs.jsx
+++ b/oauth/src/components/RecordTabs.jsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import RecordList from './RecordList.jsx'
import ChatRecordList from './ChatRecordList.jsx'
+import ProfileRecordList from './ProfileRecordList.jsx'
import LoadingSkeleton from './LoadingSkeleton.jsx'
export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, userChatRecords, userChatLoading, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) {
@@ -60,6 +61,12 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
>
comment ({filteredUserComments.length})
+
@@ -121,6 +128,14 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
/>
)
)}
+ {activeTab === 'profiles' && (
+
+ )}