import type { ChatMessage, Profile } from '../types' import { renderMarkdown } from '../lib/markdown' import { getCurrentLang } from './mode-tabs' // Get translated content for a chat message function getTranslatedContent(msg: ChatMessage): string { const currentLang = getCurrentLang() const originalLang = msg.value.lang || 'ja' const translations = msg.value.translations if (translations && currentLang !== originalLang && translations[currentLang]) { return translations[currentLang].content || msg.value.content } return msg.value.content } // Escape HTML to prevent XSS function escapeHtml(text: string): string { const div = document.createElement('div') div.textContent = text return div.innerHTML } // Format date/time for chat function formatChatTime(dateStr: string): string { const d = new Date(dateStr) const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const hour = String(d.getHours()).padStart(2, '0') const min = String(d.getMinutes()).padStart(2, '0') return `${month}/${day} ${hour}:${min}` } // Extract rkey from AT URI function getRkeyFromUri(uri: string): string { return uri.split('/').pop() || '' } // Profile info for authors interface AuthorInfo { did: string handle: string avatarUrl?: string } // Build author info map function buildAuthorMap( userDid: string, userHandle: string, botDid: string, botHandle: string, userProfile?: Profile | null, botProfile?: Profile | null, pds?: string ): Map { const authors = new Map() // User info let userAvatarUrl = '' if (userProfile?.value.avatar) { const cid = userProfile.value.avatar.ref.$link userAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${userDid}&cid=${cid}` : `/content/${userDid}/blob/${cid}` } authors.set(userDid, { did: userDid, handle: userHandle, avatarUrl: userAvatarUrl }) // Bot info let botAvatarUrl = '' if (botProfile?.value.avatar) { const cid = botProfile.value.avatar.ref.$link botAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${botDid}&cid=${cid}` : `/content/${botDid}/blob/${cid}` } authors.set(botDid, { did: botDid, handle: botHandle, avatarUrl: botAvatarUrl }) return authors } // Render chat threads list (conversations this user started) export function renderChatThreadList( messages: ChatMessage[], userDid: string, userHandle: string, botDid: string, botHandle: string, userProfile?: Profile | null, botProfile?: Profile | null, pds?: string ): string { // Build set of all message URIs const allUris = new Set(messages.map(m => m.uri)) // Find root messages by this user: // 1. No root field (explicit start of conversation) // 2. Or root points to non-existent message (orphaned, treat as root) // For orphaned roots, only keep the oldest message per orphaned root URI const orphanedRootFirstMsg = new Map() const rootMessages: ChatMessage[] = [] for (const msg of messages) { if (msg.value.author !== userDid) continue if (!msg.value.root) { // No root = explicit conversation start rootMessages.push(msg) } else if (!allUris.has(msg.value.root)) { // Orphaned root - keep only the oldest message per orphaned root const existing = orphanedRootFirstMsg.get(msg.value.root) if (!existing || new Date(msg.value.createdAt) < new Date(existing.value.createdAt)) { orphanedRootFirstMsg.set(msg.value.root, msg) } } } // Add orphaned root representatives for (const msg of orphanedRootFirstMsg.values()) { rootMessages.push(msg) } if (rootMessages.length === 0) { return '

No chat threads yet.

' } const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) // Sort by createdAt (newest first) const sorted = [...rootMessages].sort((a, b) => new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime() ) const items = sorted.map(msg => { const authorDid = msg.value.author const time = formatChatTime(msg.value.createdAt) const rkey = getRkeyFromUri(msg.uri) const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } const avatarHtml = author.avatarUrl ? `@${escapeHtml(author.handle)}` : `
` // Truncate content for preview (use translated content, show first 3 lines) const displayContent = getTranslatedContent(msg) const lines = displayContent.split('\n').slice(0, 3) const preview = lines.join('\n') return `
${avatarHtml}
@${escapeHtml(author.handle)} ${time}
${escapeHtml(preview)}
` }).join('') return `
${items}
` } // Render single chat thread (full conversation) export function renderChatThread( messages: ChatMessage[], rootRkey: string, userDid: string, userHandle: string, botDid: string, botHandle: string, userProfile?: Profile | null, botProfile?: Profile | null, pds?: string, chatCollection: string = 'ai.syui.log.chat' ): string { // Find root message const rootUri = `at://${userDid}/${chatCollection}/${rootRkey}` const rootMsg = messages.find(m => m.uri === rootUri) if (!rootMsg) { return '

Chat thread not found.

' } // Find all messages in this thread // 1. The root message itself // 2. Messages with root === rootUri (direct children) // 3. If this is an orphaned root (root points to non-existent), find siblings with same original root const originalRoot = rootMsg.value.root const allUris = new Set(messages.map(m => m.uri)) const isOrphanedRoot = originalRoot && !allUris.has(originalRoot) const threadMessages = messages.filter(msg => { // Include the root message itself if (msg.uri === rootUri) return true // Include messages that point to this as root if (msg.value.root === rootUri) return true // If orphaned, include messages with the same original root if (isOrphanedRoot && msg.value.root === originalRoot) return true return false }) if (threadMessages.length === 0) { return '

No messages in this thread.

' } const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) // Sort by createdAt const sorted = [...threadMessages].sort((a, b) => new Date(a.value.createdAt).getTime() - new Date(b.value.createdAt).getTime() ) const items = sorted.map(msg => { const authorDid = msg.value.author const time = formatChatTime(msg.value.createdAt) const rkey = getRkeyFromUri(msg.uri) const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } const avatarHtml = author.avatarUrl ? `@${escapeHtml(author.handle)}` : `
` const displayContent = getTranslatedContent(msg) const content = renderMarkdown(displayContent) const recordLink = `/@${author.handle}/at/collection/${chatCollection}/${rkey}` return ` ` }).join('') return `
${items}
` } // Render chat list page export function renderChatListPage( messages: ChatMessage[], userDid: string, userHandle: string, botDid: string, botHandle: string, userProfile?: Profile | null, botProfile?: Profile | null, pds?: string ): string { const list = renderChatThreadList(messages, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) return `
${list}
` } // Render chat thread page export function renderChatThreadPage( messages: ChatMessage[], rootRkey: string, userDid: string, userHandle: string, botDid: string, botHandle: string, userProfile?: Profile | null, botProfile?: Profile | null, pds?: string, chatCollection: string = 'ai.syui.log.chat' ): string { const thread = renderChatThread(messages, rootRkey, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds, chatCollection) return `
${thread}
` }