mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +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 { 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,
|
||||||
|
|||||||
@ -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`)
|
||||||
|
|||||||
@ -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 ============
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 ============
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user