diff --git a/locales/en-US/chat.json b/locales/en-US/chat.json index 9e6899ed11..1ce8f01147 100644 --- a/locales/en-US/chat.json +++ b/locales/en-US/chat.json @@ -32,6 +32,8 @@ "chatList.longMessageDetail": "View Details", "clearCurrentMessages": "Clear current session messages", "compressedHistory": "Compressed History", + "compression.cancel": "Uncompress", + "compression.cancelConfirm": "Are you sure you want to uncompress? This will restore the original messages.", "compression.history": "History", "compression.summary": "Summary", "confirmClearCurrentMessages": "You are about to clear the current session messages. Once cleared, they cannot be retrieved. Please confirm your action.", diff --git a/locales/zh-CN/chat.json b/locales/zh-CN/chat.json index 1593abc18e..4d888b133c 100644 --- a/locales/zh-CN/chat.json +++ b/locales/zh-CN/chat.json @@ -32,6 +32,8 @@ "chatList.longMessageDetail": "查看详情", "clearCurrentMessages": "清空当前会话消息", "compressedHistory": "压缩历史", + "compression.cancel": "取消压缩", + "compression.cancelConfirm": "确定要取消压缩吗?这将恢复原始消息。", "compression.history": "历史记录", "compression.summary": "摘要", "confirmClearCurrentMessages": "确认清空当前会话消息吗?清空后无法恢复", diff --git a/packages/database/src/schemas/message.ts b/packages/database/src/schemas/message.ts index 77c7fb404f..0d680c8d62 100644 --- a/packages/database/src/schemas/message.ts +++ b/packages/database/src/schemas/message.ts @@ -149,6 +149,7 @@ export const messages = pgTable( index('messages_thread_id_idx').on(table.threadId), index('messages_agent_id_idx').on(table.agentId), index('messages_group_id_idx').on(table.groupId), + index('messages_message_group_id_idx').on(table.messageGroupId), ], ); diff --git a/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx b/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx index bff1a3b95f..c954f6aa1d 100644 --- a/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx +++ b/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx @@ -33,7 +33,7 @@ const Nav = memo(() => { const switchTopic = useChatStore((s) => s.switchTopic); const [openNewTopicOrSaveTopic] = useChatStore((s) => [s.openNewTopicOrSaveTopic]); - const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic); + const { mutate } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic); const handleNewTopic = () => { // If in agent sub-route, navigate back to agent chat first if (isProfileActive && agentId) { @@ -46,7 +46,6 @@ const Nav = memo(() => { diff --git a/src/features/Conversation/Messages/CompressedGroup/index.tsx b/src/features/Conversation/Messages/CompressedGroup/index.tsx index 06a9aa3863..416ae84195 100644 --- a/src/features/Conversation/Messages/CompressedGroup/index.tsx +++ b/src/features/Conversation/Messages/CompressedGroup/index.tsx @@ -10,9 +10,10 @@ import { Tabs, type TabsProps, } from '@lobehub/ui'; +import { App } from 'antd'; import { createStaticStyles, cx } from 'antd-style'; import isEqual from 'fast-deep-equal'; -import { ChevronDown, ChevronUp, History, Sparkles } from 'lucide-react'; +import { ChevronDown, ChevronUp, History, Sparkles, Undo2 } from 'lucide-react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -66,6 +67,7 @@ export interface CompressedGroupMessageProps { const CompressedGroupMessage = memo(({ id }) => { const { t } = useTranslation('chat'); + const { modal } = App.useApp(); const [activeTab, setActiveTab] = useState(() => getStoredTab(id)); const handleTabChange = useCallback( @@ -80,6 +82,16 @@ const CompressedGroupMessage = memo(({ id }) => { const toggleCompressedGroupExpanded = useConversationStore( (s) => s.toggleCompressedGroupExpanded, ); + const cancelCompression = useConversationStore((s) => s.cancelCompression); + + const handleCancelCompression = useCallback(() => { + modal.confirm({ + centered: true, + content: t('compression.cancelConfirm'), + onOk: () => cancelCompression(id), + title: t('compression.cancel'), + }); + }, [id, cancelCompression, modal, t]); const content = message?.content; const rawCompressedMessages = (message as UIChatMessage)?.compressedMessages; @@ -145,11 +157,19 @@ const CompressedGroupMessage = memo(({ id }) => { onChange={handleTabChange} variant={'rounded'} /> - toggleCompressedGroupExpanded(id)} - size={'small'} - /> + + + toggleCompressedGroupExpanded(id)} + size={'small'} + /> + )} {!showContent ? null : activeTab === 'summary' ? ( diff --git a/src/features/Conversation/store/slices/message/action/state.ts b/src/features/Conversation/store/slices/message/action/state.ts index 20143a5474..289c9603ab 100644 --- a/src/features/Conversation/store/slices/message/action/state.ts +++ b/src/features/Conversation/store/slices/message/action/state.ts @@ -13,6 +13,11 @@ import { dataSelectors } from '../../data/selectors'; * Handles message state operations like loading, collapsed, etc. */ export interface MessageStateAction { + /** + * Cancel compression and restore original messages + */ + cancelCompression: (id: string) => Promise; + /** * Copy message content to clipboard */ @@ -50,6 +55,26 @@ export const messageStateSlice: StateCreator< [], MessageStateAction > = (set, get) => ({ + cancelCompression: async (id) => { + const message = dataSelectors.getDisplayMessageById(id)(get()); + if (!message || message.role !== 'compressedGroup') return; + + const { context, replaceMessages } = get(); + if (!context.agentId || !context.topicId) return; + + // Call service to cancel compression + const { messages } = await messageService.cancelCompression({ + agentId: context.agentId, + groupId: context.groupId, + messageGroupId: id, + threadId: context.threadId, + topicId: context.topicId, + }); + + // Replace messages with restored original messages + replaceMessages(messages); + }, + copyMessage: async (id, content) => { const { hooks } = get(); diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts index 183bc108b6..40fccf486d 100644 --- a/src/locales/default/chat.ts +++ b/src/locales/default/chat.ts @@ -41,6 +41,9 @@ export default { 'chatList.longMessageDetail': 'View Details', 'clearCurrentMessages': 'Clear current session messages', 'compressedHistory': 'Compressed History', + 'compression.cancel': 'Uncompress', + 'compression.cancelConfirm': + 'Are you sure you want to uncompress? This will restore the original messages.', 'compression.history': 'History', 'compression.summary': 'Summary', 'confirmClearCurrentMessages': diff --git a/src/server/routers/lambda/message.ts b/src/server/routers/lambda/message.ts index b46f4c2db4..c6d7acd898 100644 --- a/src/server/routers/lambda/message.ts +++ b/src/server/routers/lambda/message.ts @@ -48,7 +48,32 @@ export const messageRouter = router({ return ctx.messageService.addFilesToMessage(id, fileIds, resolved); }), - count: messageProcedure + /** + * Cancel compression by deleting the compression group and restoring original messages + */ +cancelCompression: messageProcedure + .input( + z.object({ + agentId: z.string(), + groupId: z.string().nullable().optional(), + messageGroupId: z.string(), + threadId: z.string().nullable().optional(), + topicId: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const { messageGroupId, agentId, groupId, threadId, topicId } = input; + + return ctx.messageService.cancelCompression(messageGroupId, { + agentId, + groupId, + threadId, + topicId, + }); + }), + + +count: messageProcedure .input( z .object({ @@ -62,7 +87,9 @@ export const messageRouter = router({ return ctx.messageModel.count(input); }), - countWords: messageProcedure + + +countWords: messageProcedure .input( z .object({ @@ -76,12 +103,13 @@ export const messageRouter = router({ return ctx.messageModel.countWords(input); }), - /** + +/** * Create a compression group for old messages * Creates a placeholder group, marks messages as compressed * Returns messages to summarize for frontend AI generation */ - createCompressionGroup: messageProcedure +createCompressionGroup: messageProcedure .input( z.object({ agentId: z.string(), @@ -102,7 +130,9 @@ export const messageRouter = router({ }); }), - createMessage: messageProcedure + + +createMessage: messageProcedure .input(CreateNewMessageParamsSchema) .mutation(async ({ input, ctx }) => { // If there's no agentId but has sessionId, resolve agentId from sessionId @@ -115,10 +145,11 @@ export const messageRouter = router({ return ctx.messageService.createMessage({ ...input, agentId } as any); }), + /** * Finalize compression by updating the group with generated summary */ - finalizeCompression: messageProcedure +finalizeCompression: messageProcedure .input( z.object({ agentId: z.string(), diff --git a/src/server/services/message/index.ts b/src/server/services/message/index.ts index 8ec13b4a28..ba231e4b5b 100644 --- a/src/server/services/message/index.ts +++ b/src/server/services/message/index.ts @@ -359,4 +359,23 @@ export class MessageService { return { messages }; } + + /** + * Cancel compression by deleting the compression group and restoring original messages + * + * @param messageGroupId - The compression group ID to cancel + * @param context - Query options for returning updated messages + */ + async cancelCompression( + messageGroupId: string, + context: QueryOptions, + ): Promise<{ messages: UIChatMessage[]; success: boolean }> { + // Delete compression group (this also unmarks messages) + await this.compressionRepository.deleteCompressionGroup(messageGroupId); + + // Query updated messages + const messages = await this.messageModel.query(context, this.getQueryOptions()); + + return { messages, success: true }; + } } diff --git a/src/services/message/index.ts b/src/services/message/index.ts index c5ba8c0ee4..e5005df835 100644 --- a/src/services/message/index.ts +++ b/src/services/message/index.ts @@ -276,6 +276,20 @@ export class MessageService { messages: (result.messages || []) as unknown as UIChatMessage[], }; }; + + /** + * Cancel compression by deleting the compression group and restoring original messages + */ + cancelCompression = async (params: { + agentId: string; + groupId?: string | null; + messageGroupId: string; + threadId?: string | null; + topicId: string; + }): Promise<{ messages: UIChatMessage[] }> => { + const result = await lambdaClient.message.cancelCompression.mutate(params); + return { messages: (result.messages || []) as unknown as UIChatMessage[] }; + }; } export const messageService = new MessageService();