test chat edit

This commit is contained in:
2026-01-22 21:01:21 +09:00
parent 7021036a5c
commit cba0228e70
47 changed files with 385 additions and 91 deletions

View File

@@ -171,7 +171,8 @@ export function renderChatThread(
userProfile?: Profile | null,
botProfile?: Profile | null,
pds?: string,
chatCollection: string = 'ai.syui.log.chat'
chatCollection: string = 'ai.syui.log.chat',
loggedInDid?: string | null
): string {
// Find root message
const rootUri = `at://${userDid}/${chatCollection}/${rootRkey}`
@@ -223,6 +224,8 @@ export function renderChatThread(
const displayContent = getTranslatedContent(msg)
const content = renderMarkdown(displayContent)
const recordLink = `/@${author.handle}/at/collection/${chatCollection}/${rkey}`
const canEdit = loggedInDid && authorDid === loggedInDid
const editLink = `/@${userHandle}/at/chat/${rkey}/edit`
return `
<article class="chat-message">
@@ -233,6 +236,7 @@ export function renderChatThread(
<div class="chat-message-header">
<a href="/@${author.handle}" class="chat-author">@${escapeHtml(author.handle)}</a>
<a href="${recordLink}" class="chat-time">${time}</a>
${canEdit ? `<a href="${editLink}" class="chat-edit-btn">edit</a>` : ''}
</div>
<div class="chat-content">${content}</div>
</div>
@@ -269,8 +273,41 @@ export function renderChatThreadPage(
userProfile?: Profile | null,
botProfile?: Profile | null,
pds?: string,
chatCollection: string = 'ai.syui.log.chat'
chatCollection: string = 'ai.syui.log.chat',
loggedInDid?: string | null
): string {
const thread = renderChatThread(messages, rootRkey, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds, chatCollection)
const thread = renderChatThread(messages, rootRkey, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds, chatCollection, loggedInDid)
return `<div class="chat-container">${thread}</div>`
}
// Render chat edit form
export function renderChatEditForm(
message: ChatMessage,
collection: string,
userHandle: string
): string {
const rkey = message.uri.split('/').pop() || ''
const content = message.value.content
return `
<div class="chat-edit-container">
<h2>Edit Chat Message</h2>
<form class="chat-edit-form" id="chat-edit-form">
<textarea
class="chat-edit-content"
id="chat-edit-content"
rows="10"
required
>${escapeHtml(content)}</textarea>
<div class="chat-edit-footer">
<span class="chat-edit-collection">${collection}</span>
<div class="chat-edit-buttons">
<a href="/@${userHandle}/at/chat/${rkey}" class="chat-edit-cancel">Cancel</a>
<button type="submit" class="chat-edit-save" id="chat-edit-save" data-rkey="${rkey}">Save</button>
</div>
</div>
</form>
<div id="chat-edit-status" class="chat-edit-status"></div>
</div>
`
}

View File

@@ -254,8 +254,8 @@ export async function updatePost(
try {
// Fetch existing record to preserve translations
let existingTranslations = undefined
let existingCreatedAt = new Date().toISOString()
let existingTranslations: unknown = undefined
let existingCreatedAt: unknown = new Date().toISOString()
try {
const existing = await agent.com.atproto.repo.getRecord({
repo: agent.assertDid,
@@ -266,7 +266,7 @@ export async function updatePost(
const value = existing.data.value as Record<string, unknown>
existingTranslations = value.translations
if (value.createdAt) {
existingCreatedAt = value.createdAt as string
existingCreatedAt = value.createdAt
}
}
} catch {
@@ -298,6 +298,51 @@ export async function updatePost(
}
}
// Update chat message
export async function updateChat(
collection: string,
rkey: string,
content: string
): Promise<{ uri: string; cid: string } | null> {
if (!agent) return null
try {
// Fetch existing record to preserve translations and other fields
let existingRecord: Record<string, unknown> = {}
try {
const existing = await agent.com.atproto.repo.getRecord({
repo: agent.assertDid,
collection,
rkey,
})
if (existing.data.value) {
existingRecord = existing.data.value as Record<string, unknown>
}
} catch {
// Record doesn't exist
throw new Error('Record not found')
}
const record: Record<string, unknown> = {
...existingRecord,
$type: collection,
content,
}
const result = await agent.com.atproto.repo.putRecord({
repo: agent.assertDid,
collection,
rkey,
record,
})
return { uri: result.data.uri, cid: result.data.cid }
} catch (err) {
console.error('Update chat error:', err)
throw err
}
}
// Save migrated card data to ai.syui.card.old
export async function saveMigratedCardData(
user: {

View File

@@ -1,5 +1,5 @@
export interface Route {
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old' | 'rse'
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'chat-edit' | 'card' | 'card-old' | 'rse'
handle?: string
rkey?: string
service?: string
@@ -69,6 +69,12 @@ export function parseRoute(): Route {
return { type: 'rse', handle: rseMatch[1] }
}
// Chat edit: /@handle/at/chat/{rkey}/edit
const chatEditMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)\/edit$/)
if (chatEditMatch) {
return { type: 'chat-edit', handle: chatEditMatch[1], rkey: chatEditMatch[2] }
}
// Chat thread: /@handle/at/chat/{rkey}
const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/)
if (chatThreadMatch) {
@@ -117,6 +123,8 @@ export function navigate(route: Route): void {
path = `/@${route.handle}/at/chat`
} else if (route.type === 'chat-thread' && route.handle && route.rkey) {
path = `/@${route.handle}/at/chat/${route.rkey}`
} else if (route.type === 'chat-edit' && route.handle && route.rkey) {
path = `/@${route.handle}/at/chat/${route.rkey}/edit`
}
window.history.pushState({}, '', path)

View File

@@ -3,7 +3,7 @@ import './styles/card.css'
import './styles/card-migrate.css'
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getRse } from './lib/api'
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost } from './lib/auth'
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat } from './lib/auth'
import { validateRecord } from './lib/lexicon'
import { renderHeader } from './components/header'
import { renderProfile } from './components/profile'
@@ -12,7 +12,7 @@ import { renderPostForm, setupPostForm } from './components/postform'
import { renderCollectionButtons, renderServerInfo, renderServiceList, renderCollectionList, renderRecordList, renderRecordDetail } from './components/browser'
import { renderModeTabs, renderLangSelector, setupModeTabs } from './components/mode-tabs'
import { renderFooter } from './components/footer'
import { renderChatListPage, renderChatThreadPage } from './components/chat'
import { renderChatListPage, renderChatThreadPage, renderChatEditForm } from './components/chat'
import { renderCardPage } from './components/card'
import { renderRsePage } from './components/rse'
import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate'
@@ -168,7 +168,7 @@ async function render(route: Route): Promise<void> {
// Mode tabs (Blog/Browser/Post/Chat/PDS)
const activeTab = route.type === 'postpage' ? 'post' :
(route.type === 'chat' || route.type === 'chat-thread') ? 'chat' :
(route.type === 'chat' || route.type === 'chat-thread' || route.type === 'chat-edit') ? 'chat' :
(route.type === 'atbrowser' || route.type === 'service' || route.type === 'collection' || route.type === 'record' ? 'browser' : 'blog')
html += renderModeTabs(handle, activeTab, localOnly)
@@ -320,10 +320,35 @@ async function render(route: Route): Promise<void> {
langList = Array.from(chatLangs)
html += renderLangSelector(langList)
html += `<div id="content">${renderChatThreadPage(chatMessages, route.rkey, did, handle, botDid, botHandle, profile, botProfile, pds || undefined, chatCollection)}</div>`
html += `<div id="content">${renderChatThreadPage(chatMessages, route.rkey, did, handle, botDid, botHandle, profile, botProfile, pds || undefined, chatCollection, loggedInDid)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}/at/chat">chat</a></nav>`
}
} else if (route.type === 'chat-edit' && route.rkey) {
// Chat edit page
if (!config.bot) {
html += `<div id="content" class="error">Bot not configured in config.json</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
} else if (!isOwner) {
html += `<div id="content" class="error">You can only edit your own messages</div>`
html += `<nav class="back-nav"><a href="/@${handle}/at/chat">chat</a></nav>`
} else {
const botDid = config.bot.did
const chatCollection = config.chatCollection || 'ai.syui.log.chat'
// Get the specific message
const chatMessages = await getChatMessages(did, botDid, chatCollection)
const targetUri = `at://${did}/${chatCollection}/${route.rkey}`
const message = chatMessages.find(m => m.uri === targetUri)
if (!message) {
html += `<div id="content" class="error">Message not found</div>`
} else {
html += `<div id="content">${renderChatEditForm(message, chatCollection, handle)}</div>`
}
html += `<nav class="back-nav"><a href="/@${handle}/at/chat">chat</a></nav>`
}
} else {
// User page: compact collection buttons + posts (use pre-loaded collections)
html += `<div id="browser">${renderCollectionButtons(collections, handle)}</div>`
@@ -368,6 +393,12 @@ async function render(route: Route): Promise<void> {
setupPostEdit(config.collection)
}
// Setup chat edit form
if (route.type === 'chat-edit' && isOwner) {
const chatCollection = config.chatCollection || 'ai.syui.log.chat'
setupChatEdit(chatCollection, handle)
}
// Setup validate button for record detail
if (currentRecord) {
setupValidateButton(currentRecord)
@@ -564,6 +595,49 @@ function setupPostEdit(collection: string): void {
})
}
// Setup chat edit form
function setupChatEdit(collection: string, handle: string): void {
const form = document.getElementById('chat-edit-form') as HTMLFormElement
const contentInput = document.getElementById('chat-edit-content') as HTMLTextAreaElement
const saveBtn = document.getElementById('chat-edit-save') as HTMLButtonElement
const statusEl = document.getElementById('chat-edit-status') as HTMLDivElement
if (!form || !saveBtn) return
form.addEventListener('submit', async (e) => {
e.preventDefault()
const rkey = saveBtn.getAttribute('data-rkey')
if (!rkey || !contentInput) return
const content = contentInput.value.trim()
if (!content) {
alert('Content is required')
return
}
try {
saveBtn.textContent = 'Saving...'
saveBtn.disabled = true
await updateChat(collection, rkey, content)
statusEl.innerHTML = '<span class="chat-edit-success">Saved!</span>'
// Navigate back to chat thread
setTimeout(() => {
navigate({ type: 'chat-thread', handle, rkey })
}, 1000)
} catch (err) {
console.error('Update failed:', err)
statusEl.innerHTML = `<span class="chat-edit-error">Error: ${err}</span>`
saveBtn.textContent = 'Save'
saveBtn.disabled = false
}
})
}
// Initial render
render(parseRoute())

View File

@@ -2510,3 +2510,133 @@ button.tab {
line-height: 1.4;
white-space: pre-line;
}
/* Chat Edit Button */
.chat-edit-btn {
color: #888;
font-size: 0.8rem;
text-decoration: none;
margin-left: 8px;
}
.chat-edit-btn:hover {
color: var(--btn-color);
text-decoration: underline;
}
/* Chat Edit Form */
.chat-edit-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.chat-edit-container h2 {
margin-bottom: 20px;
font-size: 1.2rem;
}
.chat-edit-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.chat-edit-content {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
min-height: 200px;
}
.chat-edit-content:focus {
outline: none;
border-color: var(--btn-color);
}
.chat-edit-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-edit-collection {
color: #888;
font-size: 0.85rem;
}
.chat-edit-buttons {
display: flex;
gap: 10px;
align-items: center;
}
.chat-edit-cancel {
color: #666;
text-decoration: none;
padding: 8px 16px;
}
.chat-edit-cancel:hover {
text-decoration: underline;
}
.chat-edit-save {
background: var(--btn-color);
color: white;
border: none;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
}
.chat-edit-save:hover {
opacity: 0.9;
}
.chat-edit-save:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.chat-edit-status {
margin-top: 10px;
}
.chat-edit-success {
color: #22c55e;
}
.chat-edit-error {
color: #ef4444;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.chat-edit-btn {
color: #888;
}
.chat-edit-btn:hover {
color: var(--btn-color);
}
.chat-edit-content {
background: #1a1a1a;
border-color: #333;
color: #e0e0e0;
}
.chat-edit-content:focus {
border-color: var(--btn-color);
}
.chat-edit-cancel {
color: #999;
}
}