Refactor agent session messages to use shared hook and implement batch deletion

- Replace manual Redux logic with `useTopicMessages` hook for consistent message loading behavior
- Add `deleteMessages` method to message data sources with proper block and file cleanup
- Update `DbService` to delegate batch deletion to appropriate data source implementations
This commit is contained in:
suyao 2025-09-22 22:50:45 +08:00
parent e5aa58722c
commit f533c1a2ca
No known key found for this signature in database
6 changed files with 40 additions and 75 deletions

View File

@ -1,13 +1,11 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import ContextMenu from '@renderer/components/ContextMenu' import ContextMenu from '@renderer/components/ContextMenu'
import { useSession } from '@renderer/hooks/agents/useSession' import { useSession } from '@renderer/hooks/agents/useSession'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { getGroupedMessages } from '@renderer/services/MessagesService' import { getGroupedMessages } from '@renderer/services/MessagesService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { type Topic, TopicType } from '@renderer/types'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { memo, useEffect, useMemo } from 'react' import { memo, useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import MessageGroup from './MessageGroup' import MessageGroup from './MessageGroup'
@ -22,23 +20,10 @@ type Props = {
} }
const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => { const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
const dispatch = useAppDispatch()
const { session } = useSession(agentId, sessionId) const { session } = useSession(agentId, sessionId)
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId]) const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
const messages = useAppSelector((state) => selectMessagesForTopic(state, sessionTopicId)) // Use the same hook as Messages.tsx for consistent behavior
const messages = useTopicMessages(sessionTopicId)
// Load messages when session changes or when messages are empty
useEffect(() => {
if (sessionId) {
// Only load if we don't have messages yet
// This prevents overwriting messages that were just added
const hasMessages = messages && messages.length > 0
if (!hasMessages) {
logger.info('Loading messages for agent session', { sessionId })
dispatch(loadTopicMessagesThunk(sessionTopicId, false)) // Don't force reload if we have messages in Redux
}
}
}, [dispatch, sessionId, sessionTopicId, messages?.length])
const displayMessages = useMemo(() => { const displayMessages = useMemo(() => {
if (!messages || messages.length === 0) return [] if (!messages || messages.length === 0) return []
@ -58,6 +43,7 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
const derivedTopic = useMemo<Topic>( const derivedTopic = useMemo<Topic>(
() => ({ () => ({
id: sessionTopicId, id: sessionTopicId,
type: TopicType.Session,
assistantId: sessionAssistantId, assistantId: sessionAssistantId,
name: sessionName, name: sessionName,
createdAt: sessionCreatedAt, createdAt: sessionCreatedAt,

View File

@ -373,6 +373,15 @@ export class AgentMessageDataSource implements MessageDataSource {
// 2. Or just hide from UI without actual deletion // 2. Or just hide from UI without actual deletion
} }
async deleteMessages(topicId: string, _messageIds: string[]): Promise<void> {
// Agent session messages cannot be deleted in batch
logger.warn(`deleteMessages called for agent session ${topicId}, operation not supported`)
// In a full implementation, you might want to:
// 1. Implement batch soft delete in backend
// 2. Update local state accordingly
}
async deleteMessagesByAskId(topicId: string, _askId: string): Promise<void> { async deleteMessagesByAskId(topicId: string, _askId: string): Promise<void> {
// Agent session messages cannot be deleted // Agent session messages cannot be deleted
logger.warn(`deleteMessagesByAskId called for agent session ${topicId}, operation not supported`) logger.warn(`deleteMessagesByAskId called for agent session ${topicId}, operation not supported`)

View File

@ -86,9 +86,9 @@ class DbService implements MessageDataSource {
return source.deleteMessage(topicId, messageId) return source.deleteMessage(topicId, messageId)
} }
async deleteMessagesByAskId(topicId: string, askId: string): Promise<void> { async deleteMessages(topicId: string, messageIds: string[]): Promise<void> {
const source = this.getDataSource(topicId) const source = this.getDataSource(topicId)
return source.deleteMessagesByAskId(topicId, askId) return source.deleteMessages(topicId, messageIds)
} }
// ============ Block Operations ============ // ============ Block Operations ============

View File

@ -203,39 +203,48 @@ export class DexieMessageDataSource implements MessageDataSource {
} }
} }
async deleteMessagesByAskId(topicId: string, askId: string): Promise<void> { async deleteMessages(topicId: string, messageIds: string[]): Promise<void> {
try { try {
await db.transaction('rw', db.topics, db.message_blocks, db.files, async () => { await db.transaction('rw', db.topics, db.message_blocks, db.files, async () => {
const topic = await db.topics.get(topicId) const topic = await db.topics.get(topicId)
if (!topic) return if (!topic) return
// Find all messages with the given askId // Collect all block IDs from messages to be deleted
const messagesToDelete = topic.messages.filter((m) => m.askId === askId || m.id === askId) const allBlockIds: string[] = []
const blockIdsToDelete = messagesToDelete.flatMap((m) => m.blocks || []) const messagesToDelete: Message[] = []
for (const messageId of messageIds) {
const message = topic.messages.find((m) => m.id === messageId)
if (message) {
messagesToDelete.push(message)
if (message.blocks && message.blocks.length > 0) {
allBlockIds.push(...message.blocks)
}
}
}
// Delete blocks and handle files // Delete blocks and handle files
if (blockIdsToDelete.length > 0) { if (allBlockIds.length > 0) {
const blocks = await db.message_blocks.where('id').anyOf(blockIdsToDelete).toArray() const blocks = await db.message_blocks.where('id').anyOf(allBlockIds).toArray()
const files = blocks const files = blocks
.filter((block) => block.type === 'file' || block.type === 'image') .filter((block) => block.type === 'file' || block.type === 'image')
.map((block: any) => block.file) .map((block: any) => block.file)
.filter((file) => file !== undefined) .filter((file) => file !== undefined)
// Clean up files
if (!isEmpty(files)) { if (!isEmpty(files)) {
await Promise.all(files.map((file) => FileManager.deleteFile(file.id, false))) await Promise.all(files.map((file) => FileManager.deleteFile(file.id, false)))
} }
await db.message_blocks.bulkDelete(allBlockIds)
await db.message_blocks.bulkDelete(blockIdsToDelete)
} }
// Filter out deleted messages // Remove messages from topic
const remainingMessages = topic.messages.filter((m) => m.askId !== askId && m.id !== askId) const remainingMessages = topic.messages.filter((m) => !messageIds.includes(m.id))
await db.topics.update(topicId, { messages: remainingMessages }) await db.topics.update(topicId, { messages: remainingMessages })
}) })
store.dispatch(updateTopicUpdatedAt({ topicId })) store.dispatch(updateTopicUpdatedAt({ topicId }))
} catch (error) { } catch (error) {
logger.error(`Failed to delete messages with askId ${askId} from topic ${topicId}:`, error as Error) logger.error(`Failed to delete messages from topic ${topicId}:`, error as Error)
throw error throw error
} }
} }

View File

@ -64,9 +64,9 @@ export interface MessageDataSource {
deleteMessage(topicId: string, messageId: string): Promise<void> deleteMessage(topicId: string, messageId: string): Promise<void>
/** /**
* Delete messages by askId (user query + assistant responses) * Delete multiple messages and their blocks
*/ */
deleteMessagesByAskId(topicId: string, askId: string): Promise<void> deleteMessages(topicId: string, messageIds: string[]): Promise<void>
// ============ Block Operations ============ // ============ Block Operations ============
/** /**

View File

@ -5,10 +5,7 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { dbService } from '@renderer/services/db' import { dbService } from '@renderer/services/db'
import type { Topic } from '@renderer/types'
import { TopicType } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage' import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { isAgentSessionTopicId } from '@renderer/utils/agentSession'
import type { AppDispatch, RootState } from '../index' import type { AppDispatch, RootState } from '../index'
import { upsertManyBlocks } from '../messageBlock' import { upsertManyBlocks } from '../messageBlock'
@ -80,42 +77,6 @@ export const getRawTopicV2 = async (topicId: string): Promise<{ id: string; mess
// Phase 2.2 - Batch 2: Helper functions // Phase 2.2 - Batch 2: Helper functions
// ================================================================= // =================================================================
/**
* Get a full topic object with type information
* This builds on getRawTopicV2 to provide additional metadata
*/
export const getTopicV2 = async (topicId: string): Promise<Topic | undefined> => {
try {
const rawTopic = await dbService.getRawTopic(topicId)
if (!rawTopic) {
logger.info('Topic not found', { topicId })
return undefined
}
// Construct the full Topic object
const topic: Topic = {
id: rawTopic.id,
type: isAgentSessionTopicId(topicId) ? TopicType.Session : TopicType.Chat,
messages: rawTopic.messages,
assistantId: '', // These fields would need to be fetched from appropriate source
name: '',
createdAt: Date.now(),
updatedAt: Date.now()
}
logger.info('Retrieved topic with type via DbService', {
topicId,
type: topic.type,
messageCount: topic.messages.length
})
return topic
} catch (error) {
logger.error('Failed to get topic:', { topicId, error })
return undefined
}
}
/** /**
* Update file reference count * Update file reference count
* Only applies to Dexie data source, no-op for agent sessions * Only applies to Dexie data source, no-op for agent sessions