mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 01:30:51 +08:00
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:
parent
e5aa58722c
commit
f533c1a2ca
@ -1,13 +1,11 @@
|
||||
import { loggerService } from '@logger'
|
||||
import ContextMenu from '@renderer/components/ContextMenu'
|
||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||
import { getGroupedMessages } from '@renderer/services/MessagesService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { type Topic, TopicType } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageGroup from './MessageGroup'
|
||||
@ -22,23 +20,10 @@ type Props = {
|
||||
}
|
||||
|
||||
const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
|
||||
const messages = useAppSelector((state) => selectMessagesForTopic(state, 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])
|
||||
// Use the same hook as Messages.tsx for consistent behavior
|
||||
const messages = useTopicMessages(sessionTopicId)
|
||||
|
||||
const displayMessages = useMemo(() => {
|
||||
if (!messages || messages.length === 0) return []
|
||||
@ -58,6 +43,7 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
|
||||
const derivedTopic = useMemo<Topic>(
|
||||
() => ({
|
||||
id: sessionTopicId,
|
||||
type: TopicType.Session,
|
||||
assistantId: sessionAssistantId,
|
||||
name: sessionName,
|
||||
createdAt: sessionCreatedAt,
|
||||
|
||||
@ -373,6 +373,15 @@ export class AgentMessageDataSource implements MessageDataSource {
|
||||
// 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> {
|
||||
// Agent session messages cannot be deleted
|
||||
logger.warn(`deleteMessagesByAskId called for agent session ${topicId}, operation not supported`)
|
||||
|
||||
@ -86,9 +86,9 @@ class DbService implements MessageDataSource {
|
||||
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)
|
||||
return source.deleteMessagesByAskId(topicId, askId)
|
||||
return source.deleteMessages(topicId, messageIds)
|
||||
}
|
||||
|
||||
// ============ Block Operations ============
|
||||
|
||||
@ -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 {
|
||||
await db.transaction('rw', db.topics, db.message_blocks, db.files, async () => {
|
||||
const topic = await db.topics.get(topicId)
|
||||
if (!topic) return
|
||||
|
||||
// Find all messages with the given askId
|
||||
const messagesToDelete = topic.messages.filter((m) => m.askId === askId || m.id === askId)
|
||||
const blockIdsToDelete = messagesToDelete.flatMap((m) => m.blocks || [])
|
||||
// Collect all block IDs from messages to be deleted
|
||||
const allBlockIds: string[] = []
|
||||
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
|
||||
if (blockIdsToDelete.length > 0) {
|
||||
const blocks = await db.message_blocks.where('id').anyOf(blockIdsToDelete).toArray()
|
||||
if (allBlockIds.length > 0) {
|
||||
const blocks = await db.message_blocks.where('id').anyOf(allBlockIds).toArray()
|
||||
const files = blocks
|
||||
.filter((block) => block.type === 'file' || block.type === 'image')
|
||||
.map((block: any) => block.file)
|
||||
.filter((file) => file !== undefined)
|
||||
|
||||
// Clean up files
|
||||
if (!isEmpty(files)) {
|
||||
await Promise.all(files.map((file) => FileManager.deleteFile(file.id, false)))
|
||||
}
|
||||
|
||||
await db.message_blocks.bulkDelete(blockIdsToDelete)
|
||||
await db.message_blocks.bulkDelete(allBlockIds)
|
||||
}
|
||||
|
||||
// Filter out deleted messages
|
||||
const remainingMessages = topic.messages.filter((m) => m.askId !== askId && m.id !== askId)
|
||||
// Remove messages from topic
|
||||
const remainingMessages = topic.messages.filter((m) => !messageIds.includes(m.id))
|
||||
await db.topics.update(topicId, { messages: remainingMessages })
|
||||
})
|
||||
|
||||
store.dispatch(updateTopicUpdatedAt({ topicId }))
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,9 +64,9 @@ export interface MessageDataSource {
|
||||
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 ============
|
||||
/**
|
||||
|
||||
@ -5,10 +5,7 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
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 { isAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
|
||||
import type { AppDispatch, RootState } from '../index'
|
||||
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
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Only applies to Dexie data source, no-op for agent sessions
|
||||
|
||||
Loading…
Reference in New Issue
Block a user