fix ask-ai put
This commit is contained in:
@ -328,7 +328,7 @@ a.view-markdown:any-link {
|
|||||||
|
|
||||||
/* Article */
|
/* Article */
|
||||||
.article-container {
|
.article-container {
|
||||||
display: grid;
|
/* display: grid; */
|
||||||
grid-template-columns: 1fr 240px;
|
grid-template-columns: 1fr 240px;
|
||||||
gap: 40px;
|
gap: 40px;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
|
@ -1126,4 +1126,84 @@ body {
|
|||||||
clip: rect(0, 0, 0, 0);
|
clip: rect(0, 0, 0, 0);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 0;
|
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;
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { atproto } from './api/atproto.js'
|
||||||
import { useAuth } from './hooks/useAuth.js'
|
import { useAuth } from './hooks/useAuth.js'
|
||||||
import { useAdminData } from './hooks/useAdminData.js'
|
import { useAdminData } from './hooks/useAdminData.js'
|
||||||
import { useUserData } from './hooks/useUserData.js'
|
import { useUserData } from './hooks/useUserData.js'
|
||||||
@ -14,6 +15,8 @@ export default function App() {
|
|||||||
const { user, agent, loading: authLoading, login, logout } = useAuth()
|
const { user, agent, loading: authLoading, login, logout } = useAuth()
|
||||||
const { adminData, langRecords, commentRecords, loading: dataLoading, error, retryCount, refresh: refreshAdminData } = useAdminData()
|
const { adminData, langRecords, commentRecords, loading: dataLoading, error, retryCount, refresh: refreshAdminData } = useAdminData()
|
||||||
const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData)
|
const { userComments, chatRecords, loading: userLoading, refresh: refreshUserData } = useUserData(adminData)
|
||||||
|
const [userChatRecords, setUserChatRecords] = useState([])
|
||||||
|
const [userChatLoading, setUserChatLoading] = useState(false)
|
||||||
const pageContext = usePageContext()
|
const pageContext = usePageContext()
|
||||||
const [showAskAI, setShowAskAI] = useState(false)
|
const [showAskAI, setShowAskAI] = useState(false)
|
||||||
const [showTestUI, setShowTestUI] = 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_TEST_UI = import.meta.env.VITE_ENABLE_TEST_UI === 'true'
|
||||||
const ENABLE_DEBUG = import.meta.env.VITE_ENABLE_DEBUG === '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
|
// Event listeners for blog communication
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clear OAuth completion flag once app is loaded
|
// Clear OAuth completion flag once app is loaded
|
||||||
@ -87,31 +151,80 @@ Answer:`
|
|||||||
|
|
||||||
// Save conversation to ATProto
|
// Save conversation to ATProto
|
||||||
try {
|
try {
|
||||||
const timestamp = new Date().toISOString()
|
const now = new Date()
|
||||||
const conversationRecord = {
|
const timestamp = now.toISOString()
|
||||||
repo: user.did,
|
const rkey = timestamp.replace(/[:.]/g, '-')
|
||||||
collection: 'ai.syui.log.chat',
|
|
||||||
record: {
|
// Extract post metadata from current page
|
||||||
type: 'ai.syui.log.chat',
|
const currentUrl = window.location.href
|
||||||
question: question,
|
const postSlug = currentUrl.match(/\/posts\/([^/]+)/)?.[1] || ''
|
||||||
answer: answer,
|
const postTitle = document.title.replace(' - syui.ai', '') || ''
|
||||||
user: user ? {
|
|
||||||
did: user.did,
|
// 1. Save question record
|
||||||
handle: user.handle,
|
const questionRecord = {
|
||||||
displayName: user.displayName || user.handle
|
$type: 'ai.syui.log.chat',
|
||||||
} : null,
|
post: {
|
||||||
ai: {
|
url: currentUrl,
|
||||||
did: adminData.did,
|
slug: postSlug,
|
||||||
handle: adminData.profile?.handle,
|
title: postTitle,
|
||||||
displayName: adminData.profile?.displayName
|
date: timestamp,
|
||||||
},
|
tags: [],
|
||||||
timestamp: timestamp,
|
language: "ja"
|
||||||
createdAt: timestamp
|
},
|
||||||
}
|
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)
|
await agent.api.com.atproto.repo.putRecord({
|
||||||
console.log('Conversation saved to ATProto')
|
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) {
|
} catch (saveError) {
|
||||||
console.error('Failed to save conversation:', saveError)
|
console.error('Failed to save conversation:', saveError)
|
||||||
}
|
}
|
||||||
@ -318,6 +431,8 @@ Answer:`
|
|||||||
commentRecords={commentRecords}
|
commentRecords={commentRecords}
|
||||||
userComments={userComments}
|
userComments={userComments}
|
||||||
chatRecords={chatRecords}
|
chatRecords={chatRecords}
|
||||||
|
userChatRecords={userChatRecords}
|
||||||
|
userChatLoading={userChatLoading}
|
||||||
baseRecords={adminData.records}
|
baseRecords={adminData.records}
|
||||||
apiConfig={adminData.apiConfig}
|
apiConfig={adminData.apiConfig}
|
||||||
pageContext={pageContext}
|
pageContext={pageContext}
|
||||||
@ -326,6 +441,7 @@ Answer:`
|
|||||||
onRecordDeleted={() => {
|
onRecordDeleted={() => {
|
||||||
refreshAdminData?.()
|
refreshAdminData?.()
|
||||||
refreshUserData?.()
|
refreshUserData?.()
|
||||||
|
fetchUserChatRecords?.()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
133
oauth/src/components/ChatRecordList.jsx
Normal file
133
oauth/src/components/ChatRecordList.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import RecordList from './RecordList.jsx'
|
import RecordList from './RecordList.jsx'
|
||||||
|
import ChatRecordList from './ChatRecordList.jsx'
|
||||||
import LoadingSkeleton from './LoadingSkeleton.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')
|
const [activeTab, setActiveTab] = useState('lang')
|
||||||
|
|
||||||
// Filter records based on page context
|
// Filter records based on page context
|
||||||
@ -51,7 +52,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
|||||||
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
|
className={`tab-btn ${activeTab === 'collection' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('collection')}
|
onClick={() => setActiveTab('collection')}
|
||||||
>
|
>
|
||||||
Posts ({filteredBaseRecords.length})
|
Posts ({userChatRecords?.length || 0})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
|
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
|
||||||
@ -93,17 +94,15 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{activeTab === 'collection' && (
|
{activeTab === 'collection' && (
|
||||||
!baseRecords ? (
|
userChatLoading ? (
|
||||||
<LoadingSkeleton count={2} showTitle={true} />
|
<LoadingSkeleton count={2} showTitle={true} />
|
||||||
) : (
|
) : (
|
||||||
<RecordList
|
<ChatRecordList
|
||||||
title=""
|
chatPairs={userChatRecords}
|
||||||
records={filteredBaseRecords}
|
|
||||||
apiConfig={apiConfig}
|
apiConfig={apiConfig}
|
||||||
user={user}
|
user={user}
|
||||||
agent={agent}
|
agent={agent}
|
||||||
onRecordDeleted={onRecordDeleted}
|
onRecordDeleted={onRecordDeleted}
|
||||||
showTitle={false}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user