fix ask-ai put

This commit is contained in:
2025-06-19 19:52:31 +09:00
parent 5ce0e0fd7a
commit 004081337c
5 changed files with 359 additions and 31 deletions

View File

@ -328,7 +328,7 @@ a.view-markdown:any-link {
/* Article */
.article-container {
display: grid;
/* display: grid; */
grid-template-columns: 1fr 240px;
gap: 40px;
max-width: 1000px;

View File

@ -1126,4 +1126,84 @@ body {
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Chat Conversation Styles */
.chat-conversation {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border);
}
.chat-conversation:last-child {
border-bottom: none;
}
.chat-message.comment-style {
background: var(--background);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.chat-message.user-message.comment-style {
border-left: 4px solid var(--primary);
}
.chat-message.ai-message.comment-style {
border-left: 4px solid #ffdd00;
background: #faf8ff;
}
.message-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.message-header .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--background-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
border: 1px solid var(--border);
flex-shrink: 0;
}
.message-header .user-info {
flex: 1;
}
.message-header .display-name {
font-weight: 600;
color: var(--text);
font-size: 15px;
}
.message-header .handle {
color: var(--text-secondary);
font-size: 13px;
}
.message-header .timestamp {
color: var(--text-secondary);
font-size: 12px;
margin-top: 2px;
}
.message-content {
color: var(--text);
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.record-actions {
flex-shrink: 0;
}

View File

@ -1,4 +1,5 @@
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'
@ -14,6 +15,8 @@ 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)
@ -22,6 +25,67 @@ export default function App() {
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])
// Event listeners for blog communication
useEffect(() => {
// Clear OAuth completion flag once app is loaded
@ -87,31 +151,80 @@ Answer:`
// Save conversation to ATProto
try {
const timestamp = new Date().toISOString()
const conversationRecord = {
repo: user.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
}
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.com.atproto.repo.putRecord(conversationRecord)
console.log('Conversation saved to ATProto')
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)
}
@ -318,6 +431,8 @@ Answer:`
commentRecords={commentRecords}
userComments={userComments}
chatRecords={chatRecords}
userChatRecords={userChatRecords}
userChatLoading={userChatLoading}
baseRecords={adminData.records}
apiConfig={adminData.apiConfig}
pageContext={pageContext}
@ -326,6 +441,7 @@ Answer:`
onRecordDeleted={() => {
refreshAdminData?.()
refreshUserData?.()
fetchUserChatRecords?.()
}}
/>

View File

@ -0,0 +1,133 @@
import React from 'react'
export default function ChatRecordList({ chatPairs, apiConfig, user = null, agent = null, onRecordDeleted = null }) {
if (!chatPairs || chatPairs.length === 0) {
return (
<section>
<p>チャット履歴がありません</p>
</section>
)
}
const handleDelete = async (chatPair) => {
if (!user || !agent || !chatPair.question?.uri) return
const confirmed = window.confirm('この会話を削除しますか?')
if (!confirmed) return
try {
// Delete question record
if (chatPair.question?.uri) {
const questionUriParts = chatPair.question.uri.split('/')
await agent.api.com.atproto.repo.deleteRecord({
repo: questionUriParts[2],
collection: questionUriParts[3],
rkey: questionUriParts[4]
})
}
// Delete answer record if exists
if (chatPair.answer?.uri) {
const answerUriParts = chatPair.answer.uri.split('/')
await agent.api.com.atproto.repo.deleteRecord({
repo: answerUriParts[2],
collection: answerUriParts[3],
rkey: answerUriParts[4]
})
}
if (onRecordDeleted) {
onRecordDeleted()
}
} catch (error) {
alert(`削除に失敗しました: ${error.message}`)
}
}
const canDelete = (chatPair) => {
return user && agent && chatPair.question?.uri && chatPair.question.value.author?.did === user.did
}
return (
<section>
{chatPairs.map((chatPair, i) => (
<div key={chatPair.rkey} className="chat-conversation">
{/* Question */}
{chatPair.question && (
<div className="chat-message user-message comment-style">
<div className="message-header">
{chatPair.question.value.author?.avatar ? (
<img
src={chatPair.question.value.author.avatar}
alt={`${chatPair.question.value.author.displayName || chatPair.question.value.author.handle} avatar`}
className="avatar"
/>
) : (
<div className="avatar">
{(chatPair.question.value.author?.displayName || chatPair.question.value.author?.handle || '?').charAt(0).toUpperCase()}
</div>
)}
<div className="user-info">
<div className="display-name">{chatPair.question.value.author?.displayName || chatPair.question.value.author?.handle}</div>
<div className="handle">@{chatPair.question.value.author?.handle}</div>
<div className="timestamp">{new Date(chatPair.question.value.createdAt).toLocaleString()}</div>
</div>
{canDelete(chatPair) && (
<div className="record-actions">
<button
onClick={() => handleDelete(chatPair)}
className="btn btn-danger btn-sm"
title="Delete Conversation"
>
delete
</button>
</div>
)}
</div>
<div className="message-content">{chatPair.question.value.text}</div>
</div>
)}
{/* Answer */}
{chatPair.answer && (
<div className="chat-message ai-message comment-style">
<div className="message-header">
{chatPair.answer.value.author?.avatar ? (
<img
src={chatPair.answer.value.author.avatar}
alt={`${chatPair.answer.value.author.displayName || chatPair.answer.value.author.handle} avatar`}
className="avatar"
/>
) : (
<div className="avatar">
{(chatPair.answer.value.author?.displayName || chatPair.answer.value.author?.handle || 'AI').charAt(0).toUpperCase()}
</div>
)}
<div className="user-info">
<div className="display-name">{chatPair.answer.value.author?.displayName || chatPair.answer.value.author?.handle}</div>
<div className="handle">@{chatPair.answer.value.author?.handle}</div>
<div className="timestamp">{new Date(chatPair.answer.value.createdAt).toLocaleString()}</div>
</div>
</div>
<div className="message-content">{chatPair.answer.value.text}</div>
</div>
)}
{/* Post metadata */}
{chatPair.question?.value.post?.url && (
<div className="record-meta">
<a
href={chatPair.question.value.post.url}
target="_blank"
rel="noopener noreferrer"
className="record-url"
>
{chatPair.question.value.post.url}
</a>
</div>
)}
</div>
))}
</section>
)
}

View File

@ -1,8 +1,9 @@
import React, { useState } from 'react'
import RecordList from './RecordList.jsx'
import ChatRecordList from './ChatRecordList.jsx'
import LoadingSkeleton from './LoadingSkeleton.jsx'
export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) {
export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, userChatRecords, userChatLoading, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) {
const [activeTab, setActiveTab] = useState('lang')
// Filter records based on page context
@ -51,7 +52,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
onClick={() => setActiveTab('collection')}
>
Posts ({filteredBaseRecords.length})
Posts ({userChatRecords?.length || 0})
</button>
<button
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
@ -93,17 +94,15 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
)
)}
{activeTab === 'collection' && (
!baseRecords ? (
userChatLoading ? (
<LoadingSkeleton count={2} showTitle={true} />
) : (
<RecordList
title=""
records={filteredBaseRecords}
<ChatRecordList
chatPairs={userChatRecords}
apiConfig={apiConfig}
user={user}
agent={agent}
onRecordDeleted={onRecordDeleted}
showTitle={false}
/>
)
)}