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
This commit is contained in:
icarus 2025-10-22 07:16:26 +08:00
parent d2b25af146
commit d3d02712a4
6 changed files with 71 additions and 41 deletions

View File

@ -39,7 +39,7 @@ interface Props {
language: string
// Message Block ID
blockId: string
onSave: (newContent: string) => void
onSave: (newContent: string) => Promise<void>
}
/**

View File

@ -25,7 +25,7 @@ interface UseMermaidFixTool {
/** Mermaid code */
content: string
}
onSave: (newContent: string) => void
onSave: (newContent: string) => Promise<void>
setError: (error: unknown) => void
setTools: React.Dispatch<React.SetStateAction<ActionTool[]>>
}
@ -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 })

View File

@ -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<RootState>()

View File

@ -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
}

View File

@ -34,7 +34,7 @@ const CodeBlock: React.FC<Props> = ({ 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,

View File

@ -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<MessagesProps> = ({ 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<MessagesProps> = ({ 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
})
}
)
]