diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index aaa2ffc2c4..39e44c2efb 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -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( diff --git a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx index 9dd2fbc4f5..4d8f09b346 100644 --- a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx @@ -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 = React.memo(({ action, scrollToBottom }) => { const { language } = useSettings() const [error, setError] = useState(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 = 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 = 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 ? : 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 = React.memo(({ action, scrollToBottom }) => { const handleRegenerate = () => { setContentToCopy('') - setIsLoading(true) fetchResult() } @@ -178,13 +205,20 @@ const ActionGeneral: FC = React.memo(({ action, scrollToBottom }) => { )} - {!isContented && isLoading && } - {messageContent} + {isPreparing && } + {!isPreparing && currentAssistantMessage && ( + + )} {error && {error}} - + ) }) diff --git a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx index 3ac80a014c..a5ce31bab7 100644 --- a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 ? : 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 = ({ 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 = ({ action, scrollToBottom }) => { title={t('translate.target_language')} optionFilterProp="label" onChange={(value) => handleChangeLanguage(getLanguageByLangcode(value), alterLanguage)} - disabled={isLoading} + disabled={isStreaming} /> @@ -240,7 +268,7 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { title={t('translate.alter_language')} optionFilterProp="label" onChange={(value) => handleChangeLanguage(targetLanguage, getLanguageByLangcode(value))} - disabled={isLoading} + disabled={isStreaming} /> @@ -267,13 +295,20 @@ const ActionTranslate: FC = ({ action, scrollToBottom }) => { )} - {!isContented && isLoading && } - {messageContent} + {isPreparing && } + {!isPreparing && currentAssistantMessage && ( + + )} {error && {error}} - + ) }