mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
fix: optimize action component state management to prevent duplicate loading spinners (#12318)
* refactor: separate message extraction from rendering Extract `lastAssistantMessage` memoization separately from rendering `MessageContent` component, improving code clarity and separation of concerns. * feat: Replace manual loading state with AssistantMessageStatus tracking * refactor: Replace loading state with status enum in translation action - Add LoadingOutlined icon for preparing state - Remove AssistantMessageStatus dependency - Simplify streaming detection using local status state * feat: Add logging and status sync for translation action * feat: Refactor action component state management to be consistent with translate action Replace separate `isContented` and `isLoading` states with a single `status` state that tracks 'preparing', 'streaming', and 'finished' phases. Sync status with assistant message status and update footer loading prop accordingly. * fix: Add missing pauseTrace import to ActionTranslate component * fix: Add missing break statements in assistant message status handling * fix: Move pauseTrace call inside abort completion condition
This commit is contained in:
parent
2777af77d8
commit
d0a1512f23
@ -823,6 +823,7 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
const streamProcessorCallbacks = createStreamProcessor(callbacks)
|
||||
|
||||
const abortController = new AbortController()
|
||||
logger.silly('Add Abort Controller', { id: userMessageId })
|
||||
addAbortController(userMessageId!, () => abortController.abort())
|
||||
|
||||
await transformMessagesAndFetch(
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from '@renderer/services/AssistantService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { AssistantMessageStatus } from '@renderer/types/newMessage'
|
||||
import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
@ -34,8 +35,7 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
const { language } = useSettings()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showOriginal, setShowOriginal] = useState(false)
|
||||
const [isContented, setIsContented] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [status, setStatus] = useState<'preparing' | 'streaming' | 'finished'>('preparing')
|
||||
const [contentToCopy, setContentToCopy] = useState('')
|
||||
const initialized = useRef(false)
|
||||
|
||||
@ -96,19 +96,24 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
}, [action, language])
|
||||
|
||||
const fetchResult = useCallback(() => {
|
||||
if (!initialized.current) {
|
||||
return
|
||||
}
|
||||
setStatus('preparing')
|
||||
|
||||
const setAskId = (id: string) => {
|
||||
askId.current = id
|
||||
}
|
||||
const onStream = () => {
|
||||
setIsContented(true)
|
||||
setStatus('streaming')
|
||||
scrollToBottom?.()
|
||||
}
|
||||
const onFinish = (content: string) => {
|
||||
setStatus('finished')
|
||||
setContentToCopy(content)
|
||||
setIsLoading(false)
|
||||
}
|
||||
const onError = (error: Error) => {
|
||||
setIsLoading(false)
|
||||
setStatus('finished')
|
||||
setError(error.message)
|
||||
}
|
||||
|
||||
@ -131,17 +136,40 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
|
||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
||||
|
||||
// Memoize the messages to prevent unnecessary re-renders
|
||||
const messageContent = useMemo(() => {
|
||||
const currentAssistantMessage = useMemo(() => {
|
||||
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')
|
||||
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]
|
||||
return lastAssistantMessage ? <MessageContent key={lastAssistantMessage.id} message={lastAssistantMessage} /> : null
|
||||
if (assistantMessages.length === 0) {
|
||||
return null
|
||||
}
|
||||
return assistantMessages[assistantMessages.length - 1]
|
||||
}, [allMessages])
|
||||
|
||||
useEffect(() => {
|
||||
// Sync message status
|
||||
switch (currentAssistantMessage?.status) {
|
||||
case AssistantMessageStatus.PROCESSING:
|
||||
case AssistantMessageStatus.PENDING:
|
||||
case AssistantMessageStatus.SEARCHING:
|
||||
setStatus('streaming')
|
||||
break
|
||||
case AssistantMessageStatus.PAUSED:
|
||||
case AssistantMessageStatus.ERROR:
|
||||
case AssistantMessageStatus.SUCCESS:
|
||||
setStatus('finished')
|
||||
break
|
||||
case undefined:
|
||||
break
|
||||
default:
|
||||
logger.warn('Unexpected assistant message status:', { status: currentAssistantMessage?.status })
|
||||
}
|
||||
}, [currentAssistantMessage?.status])
|
||||
|
||||
const isPreparing = status === 'preparing'
|
||||
const isStreaming = status === 'streaming'
|
||||
|
||||
const handlePause = () => {
|
||||
if (askId.current) {
|
||||
abortCompletion(askId.current)
|
||||
setIsLoading(false)
|
||||
}
|
||||
if (topicRef.current?.id) {
|
||||
pauseTrace(topicRef.current.id)
|
||||
@ -150,7 +178,6 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
|
||||
const handleRegenerate = () => {
|
||||
setContentToCopy('')
|
||||
setIsLoading(true)
|
||||
fetchResult()
|
||||
}
|
||||
|
||||
@ -178,13 +205,20 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
</OriginalContent>
|
||||
)}
|
||||
<Result>
|
||||
{!isContented && isLoading && <LoadingOutlined style={{ fontSize: 16 }} spin />}
|
||||
{messageContent}
|
||||
{isPreparing && <LoadingOutlined style={{ fontSize: 16 }} spin />}
|
||||
{!isPreparing && currentAssistantMessage && (
|
||||
<MessageContent key={currentAssistantMessage.id} message={currentAssistantMessage} />
|
||||
)}
|
||||
</Result>
|
||||
{error && <ErrorMsg>{error}</ErrorMsg>}
|
||||
</Container>
|
||||
<FooterPadding />
|
||||
<WindowFooter loading={isLoading} onPause={handlePause} onRegenerate={handleRegenerate} content={contentToCopy} />
|
||||
<WindowFooter
|
||||
loading={isStreaming}
|
||||
onPause={handlePause}
|
||||
onRegenerate={handleRegenerate}
|
||||
content={contentToCopy}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@ -9,7 +9,9 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import type { Assistant, Topic, TranslateLanguage, TranslateLanguageCode } from '@renderer/types'
|
||||
import { AssistantMessageStatus } from '@renderer/types/newMessage'
|
||||
import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { detectLanguage } from '@renderer/utils/translate'
|
||||
@ -48,8 +50,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
|
||||
const [error, setError] = useState('')
|
||||
const [showOriginal, setShowOriginal] = useState(false)
|
||||
const [isContented, setIsContented] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [status, setStatus] = useState<'preparing' | 'streaming' | 'finished'>('preparing')
|
||||
const [contentToCopy, setContentToCopy] = useState('')
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
@ -106,6 +107,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
// Initialize language pair.
|
||||
// It will update targetLangRef, so we could get latest target language in the following code
|
||||
await updateLanguagePair()
|
||||
logger.silly('[initialize] UpdateLanguagePair completed.')
|
||||
|
||||
// Initialize assistant
|
||||
const currentAssistant = getDefaultTranslateAssistant(targetLangRef.current, action.selectedText)
|
||||
@ -132,20 +134,18 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
askId.current = id
|
||||
}
|
||||
const onStream = () => {
|
||||
setIsContented(true)
|
||||
setStatus('streaming')
|
||||
scrollToBottom?.()
|
||||
}
|
||||
const onFinish = (content: string) => {
|
||||
setStatus('finished')
|
||||
setContentToCopy(content)
|
||||
setIsLoading(false)
|
||||
}
|
||||
const onError = (error: Error) => {
|
||||
setIsLoading(false)
|
||||
setStatus('finished')
|
||||
setError(error.message)
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
let sourceLanguageCode: TranslateLanguageCode
|
||||
|
||||
try {
|
||||
@ -182,12 +182,37 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
|
||||
const allMessages = useTopicMessages(topicRef.current?.id || '')
|
||||
|
||||
const messageContent = useMemo(() => {
|
||||
const currentAssistantMessage = useMemo(() => {
|
||||
const assistantMessages = allMessages.filter((message) => message.role === 'assistant')
|
||||
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]
|
||||
return lastAssistantMessage ? <MessageContent key={lastAssistantMessage.id} message={lastAssistantMessage} /> : null
|
||||
if (assistantMessages.length === 0) {
|
||||
return null
|
||||
}
|
||||
return assistantMessages[assistantMessages.length - 1]
|
||||
}, [allMessages])
|
||||
|
||||
useEffect(() => {
|
||||
// Sync message status
|
||||
switch (currentAssistantMessage?.status) {
|
||||
case AssistantMessageStatus.PROCESSING:
|
||||
case AssistantMessageStatus.PENDING:
|
||||
case AssistantMessageStatus.SEARCHING:
|
||||
setStatus('streaming')
|
||||
break
|
||||
case AssistantMessageStatus.PAUSED:
|
||||
case AssistantMessageStatus.ERROR:
|
||||
case AssistantMessageStatus.SUCCESS:
|
||||
setStatus('finished')
|
||||
break
|
||||
case undefined:
|
||||
break
|
||||
default:
|
||||
logger.warn('Unexpected assistant message status:', { status: currentAssistantMessage?.status })
|
||||
}
|
||||
}, [currentAssistantMessage?.status])
|
||||
|
||||
const isPreparing = status === 'preparing'
|
||||
const isStreaming = status === 'streaming'
|
||||
|
||||
const handleChangeLanguage = (targetLanguage: TranslateLanguage, alterLanguage: TranslateLanguage) => {
|
||||
if (!initialized) {
|
||||
return
|
||||
@ -200,15 +225,18 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
// FIXME: It doesn't work because abort signal is not set.
|
||||
logger.silly('Try to pause: ', { id: askId.current })
|
||||
if (askId.current) {
|
||||
abortCompletion(askId.current)
|
||||
setIsLoading(false)
|
||||
}
|
||||
if (topicRef.current?.id) {
|
||||
pauseTrace(topicRef.current.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegenerate = () => {
|
||||
setContentToCopy('')
|
||||
setIsLoading(true)
|
||||
fetchResult()
|
||||
}
|
||||
|
||||
@ -228,7 +256,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
title={t('translate.target_language')}
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)}
|
||||
disabled={isLoading}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</Tooltip>
|
||||
<ArrowRightFromLine size={16} color="var(--color-text-3)" style={{ margin: '0 2px' }} />
|
||||
@ -240,7 +268,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
title={t('translate.alter_language')}
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))}
|
||||
disabled={isLoading}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" title={t('selection.action.translate.smart_translate_tips')} arrow>
|
||||
@ -267,13 +295,20 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
|
||||
</OriginalContent>
|
||||
)}
|
||||
<Result>
|
||||
{!isContented && isLoading && <LoadingOutlined style={{ fontSize: 16 }} spin />}
|
||||
{messageContent}
|
||||
{isPreparing && <LoadingOutlined style={{ fontSize: 16 }} spin />}
|
||||
{!isPreparing && currentAssistantMessage && (
|
||||
<MessageContent key={currentAssistantMessage.id} message={currentAssistantMessage} />
|
||||
)}
|
||||
</Result>
|
||||
{error && <ErrorMsg>{error}</ErrorMsg>}
|
||||
</Container>
|
||||
<FooterPadding />
|
||||
<WindowFooter loading={isLoading} onPause={handlePause} onRegenerate={handleRegenerate} content={contentToCopy} />
|
||||
<WindowFooter
|
||||
loading={isStreaming}
|
||||
onPause={handlePause}
|
||||
onRegenerate={handleRegenerate}
|
||||
content={contentToCopy}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user