mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
fix(TopicsTab): persist pending state via Redux and update after task completion (#8376)
* fix(TopicsTab): pending状态将自动关闭并不再由组件管理 * feat(消息状态): 添加话题完成状态指示器及相关逻辑 - 在消息状态中新增fulfilledByTopic字段记录话题完成状态 - 添加setTopicFulfilled action用于更新话题完成状态 - 在话题切换时重置完成状态为false - 在加载完成后设置完成状态为true - 添加完成状态指示器组件并显示在话题列表中 * fix(TopicsTab): 修复不切换话题时未重置当前话题fulfilled状态的问题 * refactor(messageThunk): 重命名 handleChangeLoadingOfTopic 为 finishTopicLoading 提高函数命名清晰度,更准确描述其功能
This commit is contained in:
parent
75b8a5a6a7
commit
622d15d5d7
@ -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])
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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<Props> = ({ 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<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
|
||||
const pendingTopics = useMemo(() => {
|
||||
return new Set<string>()
|
||||
}, [])
|
||||
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<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
onClick={() => onSwitchTopic(topic)}
|
||||
style={{ borderRadius }}>
|
||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||
<TopicNameContainer>
|
||||
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||
{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`
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export interface MessagesState extends EntityState<Message, string> {
|
||||
messageIdsByTopic: Record<string, string[]> // Map: topicId -> ordered message IDs
|
||||
currentTopicId: string | null
|
||||
loadingByTopic: Record<string, boolean>
|
||||
fulfilledByTopic: Record<string, boolean>
|
||||
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<SetTopicFulfilledPayload>) {
|
||||
const { topicId, fulfilled } = action.payload
|
||||
state.fulfilledByTopic[topicId] = fulfilled
|
||||
},
|
||||
setDisplayCount(state, action: PayloadAction<number>) {
|
||||
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<InsertMessageAtIndexPayload>) {
|
||||
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<RemoveMessagePayload>) {
|
||||
const { topicId, messageId } = action.payload
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user