diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index abb22cc921..d2a71622fd 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -18,6 +18,8 @@ import { getStoreSetting } from './useSettings' let _activeTopic: Topic let _setActiveTopic: (topic: Topic) => void +// const logger = loggerService.withContext('useTopic') + export function useActiveTopic(assistantId: string, topic?: Topic) { const { assistant } = useAssistant(assistantId) const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0]) diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index 66aa73c852..bffe21fdff 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -3,8 +3,10 @@ import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings' import { useActiveTopic } from '@renderer/hooks/useTopic' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import NavigationService from '@renderer/services/NavigationService' +import { newMessagesActions } from '@renderer/store/newMessage' import { Assistant, Topic } from '@renderer/types' import { FC, startTransition, useCallback, useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -25,6 +27,7 @@ const HomePage: FC = () => { const [activeAssistant, _setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic) const { showAssistants, showTopics, topicPosition } = useSettings() + const dispatch = useDispatch() _activeAssistant = activeAssistant @@ -43,9 +46,12 @@ const HomePage: FC = () => { const setActiveTopic = useCallback( (newTopic: Topic) => { - startTransition(() => _setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic))) + startTransition(() => { + _setActiveTopic((prev) => (newTopic?.id === prev.id ? prev : newTopic)) + dispatch(newMessagesActions.setTopicFulfilled({ topicId: newTopic.id, fulfilled: false })) + }) }, - [_setActiveTopic] + [_setActiveTopic, dispatch] ) useEffect(() => { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 1f3997ff8a..b8e523c6d8 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -23,6 +23,7 @@ import { fetchMessagesSummary } from '@renderer/services/ApiService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import store from '@renderer/store' import { RootState } from '@renderer/store' +import { newMessagesActions } from '@renderer/store/newMessage' import { setGenerating } from '@renderer/store/runtime' import { Assistant, Topic } from '@renderer/types' import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils' @@ -35,16 +36,17 @@ import { exportTopicToNotion, topicToMarkdown } from '@renderer/utils/export' -import { hasTopicPendingRequests } from '@renderer/utils/queue' import { Dropdown, MenuProps, Tooltip } from 'antd' import { ItemType, MenuItemType } from 'antd/es/menu/interface' import dayjs from 'dayjs' import { findIndex } from 'lodash' -import { FC, useCallback, useDeferredValue, useMemo, useRef, useState } from 'react' +import { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' +// const logger = loggerService.withContext('TopicsTab') + interface Props { assistant: Assistant activeTopic: Topic @@ -59,6 +61,8 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, const { showTopicTime, pinTopicsToTop, setTopicPosition, topicPosition } = useSettings() const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics) + const topicLoadingQuery = useSelector((state: RootState) => state.messages.loadingByTopic) + const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic) const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' @@ -66,27 +70,13 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, const [deletingTopicId, setDeletingTopicId] = useState(null) const deleteTimerRef = useRef(null) - const pendingTopics = useMemo(() => { - return new Set() - }, []) - const isPending = useCallback( - (topicId: string) => { - const hasPending = hasTopicPendingRequests(topicId) - if (topicId === activeTopic.id && !hasPending) { - pendingTopics.delete(topicId) - return false - } - if (pendingTopics.has(topicId)) { - return true - } - if (hasPending) { - pendingTopics.add(topicId) - return true - } - return false - }, - [activeTopic.id, pendingTopics] - ) + const isPending = useCallback((topicId: string) => topicLoadingQuery[topicId], [topicLoadingQuery]) + const isFulfilled = useCallback((topicId: string) => topicFulfilledQuery[topicId], [topicFulfilledQuery]) + const dispatch = useDispatch() + + useEffect(() => { + dispatch(newMessagesActions.setTopicFulfilled({ topicId: activeTopic.id, fulfilled: false })) + }, [activeTopic.id, dispatch, topicFulfilledQuery]) const isRenaming = useCallback( (topicId: string) => { @@ -480,6 +470,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, onClick={() => onSwitchTopic(topic)} style={{ borderRadius }}> {isPending(topic.id) && !isActive && } + {isFulfilled(topic.id) && !isActive && } {topicName} @@ -644,7 +635,20 @@ const PendingIndicator = styled.div.attrs({ left: 3px; top: 15px; border-radius: 50%; - background-color: var(--color-primary); + background-color: var(--color-status-warning); +` + +const FulfilledIndicator = styled.div.attrs({ + className: 'animation-pulse' +})` + --pulse-size: 5px; + width: 5px; + height: 5px; + position: absolute; + left: 3px; + top: 15px; + border-radius: 50%; + background-color: var(--color-status-success); ` const AddTopicButton = styled.div` diff --git a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts index d8ae1598e4..0d61ab106b 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts @@ -212,8 +212,8 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => { }) ) await saveUpdatesToDB(assistantMsgId, topicId, messageUpdates, []) - EventEmitter.emit(EVENT_NAMES.MESSAGE_COMPLETE, { id: assistantMsgId, topicId, status }) + logger.debug('onComplete finished') } } } diff --git a/src/renderer/src/store/newMessage.ts b/src/renderer/src/store/newMessage.ts index 38f44bec6e..9b85a8aab7 100644 --- a/src/renderer/src/store/newMessage.ts +++ b/src/renderer/src/store/newMessage.ts @@ -14,6 +14,7 @@ export interface MessagesState extends EntityState { messageIdsByTopic: Record // Map: topicId -> ordered message IDs currentTopicId: string | null loadingByTopic: Record + fulfilledByTopic: Record displayCount: number } @@ -22,6 +23,7 @@ const initialState: MessagesState = messagesAdapter.getInitialState({ messageIdsByTopic: {}, currentTopicId: null, loadingByTopic: {}, + fulfilledByTopic: {}, displayCount: 20 }) @@ -37,6 +39,12 @@ interface SetTopicLoadingPayload { loading: boolean } +// Payload for setting topic loading state +interface SetTopicFulfilledPayload { + topicId: string + fulfilled: boolean +} + // Payload for upserting a block reference interface UpsertBlockReferencePayload { messageId: string @@ -85,6 +93,10 @@ export const messagesSlice = createSlice({ const { topicId, loading } = action.payload state.loadingByTopic[topicId] = loading }, + setTopicFulfilled(state, action: PayloadAction) { + const { topicId, fulfilled } = action.payload + state.fulfilledByTopic[topicId] = fulfilled + }, setDisplayCount(state, action: PayloadAction) { state.displayCount = action.payload }, @@ -104,6 +116,9 @@ export const messagesSlice = createSlice({ if (!(topicId in state.loadingByTopic)) { state.loadingByTopic[topicId] = false } + if (!(topicId in state.fulfilledByTopic)) { + state.fulfilledByTopic[topicId] = false + } }, insertMessageAtIndex(state, action: PayloadAction) { const { topicId, message, index } = action.payload @@ -118,6 +133,9 @@ export const messagesSlice = createSlice({ if (!(topicId in state.loadingByTopic)) { state.loadingByTopic[topicId] = false } + if (!(topicId in state.fulfilledByTopic)) { + state.fulfilledByTopic[topicId] = false + } }, updateMessage( state, @@ -162,6 +180,7 @@ export const messagesSlice = createSlice({ } delete state.messageIdsByTopic[topicId] state.loadingByTopic[topicId] = false + state.fulfilledByTopic[topicId] = false }, removeMessage(state, action: PayloadAction) { const { topicId, messageId } = action.payload diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 415523a6e0..1a55e5f2c9 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -29,9 +29,10 @@ import { newMessagesActions, selectMessagesForTopic } from '../newMessage' const logger = loggerService.withContext('MessageThunk') -const handleChangeLoadingOfTopic = async (topicId: string) => { +const finishTopicLoading = async (topicId: string) => { await waitForTopicQueue(topicId) store.dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false })) + store.dispatch(newMessagesActions.setTopicFulfilled({ topicId, fulfilled: true })) } // TODO: 后续可以将db操作移到Listener Middleware中 export const saveMessageAndBlocksToDB = async (message: Message, blocks: MessageBlock[], messageIndex: number = -1) => { @@ -956,7 +957,7 @@ export const sendMessage = } catch (error) { logger.error('Error in sendMessage thunk:', error as Error) } finally { - handleChangeLoadingOfTopic(topicId) + finishTopicLoading(topicId) } } @@ -1213,7 +1214,7 @@ export const resendMessageThunk = } catch (error) { logger.error(`[resendMessageThunk] Error resending user message ${userMessageToResend.id}:`, error as Error) } finally { - handleChangeLoadingOfTopic(topicId) + finishTopicLoading(topicId) } } @@ -1347,7 +1348,7 @@ export const regenerateAssistantResponseThunk = ) // dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false })) } finally { - handleChangeLoadingOfTopic(topicId) + finishTopicLoading(topicId) } } @@ -1524,7 +1525,7 @@ export const appendAssistantResponseThunk = // Optionally dispatch an error action or notification // Resetting loading state should be handled by the underlying fetchAndProcessAssistantResponseImpl } finally { - handleChangeLoadingOfTopic(topicId) + finishTopicLoading(topicId) } }