From d3d02712a46ae656bb66f0e075965c1be3562ec4 Mon Sep 17 00:00:00 2001 From: icarus Date: Wed, 22 Oct 2025 07:16:26 +0800 Subject: [PATCH] refactor(code-block): extract code block editing logic to custom hook Move code block editing functionality from Messages component to useEditCodeBlock hook for better reusability and maintainability --- .../src/components/CodeBlockView/view.tsx | 2 +- .../CodeToolbar/hooks/useMermaidFixTool.tsx | 4 +- src/renderer/src/hooks/useChatContext.ts | 1 + src/renderer/src/hooks/useEditCodeBlock.ts | 55 +++++++++++++++++++ .../src/pages/home/Markdown/CodeBlock.tsx | 2 +- .../src/pages/home/Messages/Messages.tsx | 48 ++++------------ 6 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 src/renderer/src/hooks/useEditCodeBlock.ts diff --git a/src/renderer/src/components/CodeBlockView/view.tsx b/src/renderer/src/components/CodeBlockView/view.tsx index c703578382..25c5a76fdf 100644 --- a/src/renderer/src/components/CodeBlockView/view.tsx +++ b/src/renderer/src/components/CodeBlockView/view.tsx @@ -39,7 +39,7 @@ interface Props { language: string // Message Block ID blockId: string - onSave: (newContent: string) => void + onSave: (newContent: string) => Promise } /** diff --git a/src/renderer/src/components/CodeToolbar/hooks/useMermaidFixTool.tsx b/src/renderer/src/components/CodeToolbar/hooks/useMermaidFixTool.tsx index 1a7c17b655..30ed956f45 100644 --- a/src/renderer/src/components/CodeToolbar/hooks/useMermaidFixTool.tsx +++ b/src/renderer/src/components/CodeToolbar/hooks/useMermaidFixTool.tsx @@ -25,7 +25,7 @@ interface UseMermaidFixTool { /** Mermaid code */ content: string } - onSave: (newContent: string) => void + onSave: (newContent: string) => Promise setError: (error: unknown) => void setTools: React.Dispatch> } @@ -136,7 +136,7 @@ export const useMermaidFixTool = ({ enabled, context, onSave, setError, setTools if (parsedResult.success) { const validResult = parsedResult.data if (validResult.fixed) { - onSave(validResult.result) + await onSave(validResult.result) setError(undefined) } else { window.toast.warning({ title: t('code_block.mermaid_fix.failed'), description: validResult.reason }) diff --git a/src/renderer/src/hooks/useChatContext.ts b/src/renderer/src/hooks/useChatContext.ts index f4ed5505d2..ba49a0d454 100644 --- a/src/renderer/src/hooks/useChatContext.ts +++ b/src/renderer/src/hooks/useChatContext.ts @@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next' import { useStore } from 'react-redux' const logger = loggerService.withContext('useChatContext') +// TODO: use useContext to refactor it. export const useChatContext = (activeTopic: Topic) => { const { t } = useTranslation() const store = useStore() diff --git a/src/renderer/src/hooks/useEditCodeBlock.ts b/src/renderer/src/hooks/useEditCodeBlock.ts new file mode 100644 index 0000000000..655a9e8932 --- /dev/null +++ b/src/renderer/src/hooks/useEditCodeBlock.ts @@ -0,0 +1,55 @@ +import { loggerService } from '@logger' +import store, { useAppDispatch } from '@renderer/store' +import { messageBlocksSelectors, updateOneBlock } from '@renderer/store/messageBlock' +import { updateMessageAndBlocksThunk } from '@renderer/store/thunk/messageThunk' +import type { MessageBlock } from '@renderer/types/newMessage' +import { MessageBlockType } from '@renderer/types/newMessage' +import { updateCodeBlock } from '@renderer/utils/markdown' +import { isTextLikeBlock } from '@renderer/utils/messageUtils/is' +import { t } from 'i18next' +import { useCallback } from 'react' + +const logger = loggerService.withContext('useEditCodeBlock') + +export const useEditCodeBlock = () => { + const dispatch = useAppDispatch() + + const editCodeBlock = useCallback( + async (data: { topicId: string; msgBlockId: string; codeBlockId: string; newContent: string }) => { + const { topicId, msgBlockId, codeBlockId, newContent } = data + + const msgBlock = messageBlocksSelectors.selectById(store.getState(), msgBlockId) + + // FIXME: 目前 error block 没有 content + if (msgBlock && isTextLikeBlock(msgBlock) && msgBlock.type !== MessageBlockType.ERROR) { + try { + const updatedRaw = updateCodeBlock(msgBlock.content, codeBlockId, newContent) + const updatedBlock: MessageBlock = { + ...msgBlock, + content: updatedRaw, + updatedAt: new Date().toISOString() + } + + dispatch(updateOneBlock({ id: msgBlockId, changes: { content: updatedRaw } })) + await dispatch(updateMessageAndBlocksThunk(topicId, null, [updatedBlock])) + + window.toast.success(t('code_block.edit.save.success')) + } catch (error) { + logger.error( + `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}:`, + error as Error + ) + window.toast.error(t('code_block.edit.save.failed.label')) + } + } else { + logger.error( + `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}: no such message block or the block doesn't have a content field` + ) + window.toast.error(t('code_block.edit.save.failed.label')) + } + }, + [dispatch] + ) + + return editCodeBlock +} diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index 58b6e6d127..321da3c265 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -34,7 +34,7 @@ const CodeBlock: React.FC = ({ children, className, node, blockId }) => { const isStreaming = useMemo(() => msgBlock?.status === MessageBlockStatus.STREAMING, [msgBlock?.status]) const handleSave = useCallback( - (newContent: string) => { + async (newContent: string) => { if (id !== undefined) { EventEmitter.emit(EVENT_NAMES.EDIT_CODE_BLOCK, { msgBlockId: blockId, diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 2f0eaf07ed..88cd460e56 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -5,6 +5,7 @@ import { LoadingIcon } from '@renderer/components/Icons' import { LOAD_MORE_COUNT } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { useChatContext } from '@renderer/hooks/useChatContext' +import { useEditCodeBlock } from '@renderer/hooks/useEditCodeBlock' import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useShortcut } from '@renderer/hooks/useShortcuts' @@ -15,22 +16,18 @@ import { getDefaultTopic } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService' import { estimateHistoryTokens } from '@renderer/services/TokenService' -import store, { useAppDispatch } from '@renderer/store' -import { messageBlocksSelectors, updateOneBlock } from '@renderer/store/messageBlock' +import { useAppDispatch } from '@renderer/store' import { newMessagesActions } from '@renderer/store/newMessage' -import { saveMessageAndBlocksToDB, updateMessageAndBlocksThunk } from '@renderer/store/thunk/messageThunk' +import { saveMessageAndBlocksToDB } from '@renderer/store/thunk/messageThunk' import type { Assistant, Topic } from '@renderer/types' -import type { MessageBlock } from '@renderer/types/newMessage' -import { type Message, MessageBlockType } from '@renderer/types/newMessage' +import { type Message } from '@renderer/types/newMessage' import { captureScrollableAsBlob, captureScrollableAsDataURL, removeSpecialCharactersForFileName, runAsyncFunction } from '@renderer/utils' -import { updateCodeBlock } from '@renderer/utils/markdown' import { getMainTextContent } from '@renderer/utils/messageUtils/find' -import { isTextLikeBlock } from '@renderer/utils/messageUtils/is' import { last } from 'lodash' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -70,6 +67,7 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o const messages = useTopicMessages(topic.id) const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic) const { setTimeoutTimer } = useTimer() + const editCodeBlock = useEditCodeBlock() const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic) @@ -205,36 +203,12 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o EVENT_NAMES.EDIT_CODE_BLOCK, async (data: { msgBlockId: string; codeBlockId: string; newContent: string }) => { const { msgBlockId, codeBlockId, newContent } = data - - const msgBlock = messageBlocksSelectors.selectById(store.getState(), msgBlockId) - - // FIXME: 目前 error block 没有 content - if (msgBlock && isTextLikeBlock(msgBlock) && msgBlock.type !== MessageBlockType.ERROR) { - try { - const updatedRaw = updateCodeBlock(msgBlock.content, codeBlockId, newContent) - const updatedBlock: MessageBlock = { - ...msgBlock, - content: updatedRaw, - updatedAt: new Date().toISOString() - } - - dispatch(updateOneBlock({ id: msgBlockId, changes: { content: updatedRaw } })) - await dispatch(updateMessageAndBlocksThunk(topic.id, null, [updatedBlock])) - - window.toast.success(t('code_block.edit.save.success')) - } catch (error) { - logger.error( - `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}:`, - error as Error - ) - window.toast.error(t('code_block.edit.save.failed.label')) - } - } else { - logger.error( - `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}: no such message block or the block doesn't have a content field` - ) - window.toast.error(t('code_block.edit.save.failed.label')) - } + return editCodeBlock({ + topicId: topic.id, + msgBlockId, + codeBlockId, + newContent + }) } ) ]