pert: Optimize/message structure (#3136)
* refactor: Simplify message operations with new useMessageOperations hook - Introduce useMessageOperations hook to centralize message-related actions - Remove prop drilling for message deletion and management - Refactor MessageMenubar, MessageGroup, and Messages components to use new hook - Remove commented-out code and simplify message state management - Improve type safety and reduce component complexity * feat: Enhance topic management with sequence-based sorting and lazy loading - Add sequence field to topics for better sorting - Implement lazy loading mechanism for topic messages - Modify Redux store to support per-topic loading states - Update database schema to use sequence as an auto-incrementing primary key - Optimize message initialization and retrieval process * refactor: Simplify message operations with new useMessageOperations hook - Introduce useMessageOperations hook to centralize message-related actions - Remove prop drilling for message deletion and management - Refactor MessageMenubar, MessageGroup, and Messages components to use new hook - Remove commented-out code and simplify message state management - Improve type safety and reduce component complexity * refactor(database): Enhance topic management with timestamps and upgrade logic - Modify database schema to include createdAt and updatedAt for topics - Add database hooks for automatic timestamp handling - Refactor topic upgrade process to support new timestamp fields - Remove redundant upgradesV6.ts file - Update topic retrieval to use updatedAt for sorting - Improve database consistency and tracking of topic modifications * fix: Improve message loading state management and UI synchronization - Update Inputbar to use useMessageOperations hook for loading state - Correct topic loading state management in Redux store - Fix loading state synchronization in sendMessage action - Remove unnecessary commented-out code - Enhance error handling and loading state tracking * refactor: Streamline message state management and remove unused code - Remove commented-out code in multiple components - Delete initializeMessagesState thunk from messages store - Simplify message sending and streaming logic - Remove unnecessary console logs - Optimize MessageStream component with memo - Using loading to control message generation within a single session - Lift the restriction on not being able to switch topics in message generation
This commit is contained in:
parent
56dd2d17e7
commit
2fd3ebb378
@ -1,8 +1,7 @@
|
||||
import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types'
|
||||
import { Dexie, type EntityTable } from 'dexie'
|
||||
|
||||
import { upgradeToV5 } from './upgrades'
|
||||
|
||||
import { upgradeToV5, upgradeToV6 } from './upgrades'
|
||||
// Database declaration (move this to its own module also)
|
||||
export const db = new Dexie('CherryStudio') as Dexie & {
|
||||
files: EntityTable<FileType, 'id'>
|
||||
@ -47,4 +46,27 @@ db.version(5)
|
||||
})
|
||||
.upgrade((tx) => upgradeToV5(tx))
|
||||
|
||||
db.version(6)
|
||||
.stores({
|
||||
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||
topics: '&id, messages, createdAt, updatedAt',
|
||||
settings: '&id, value',
|
||||
knowledge_notes: '&id, baseId, type, content, created_at, updated_at',
|
||||
translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt'
|
||||
})
|
||||
.upgrade((tx) => upgradeToV6(tx))
|
||||
|
||||
// Add hooks for automatic timestamp handling
|
||||
db.topics.hook('creating', (_, obj: any) => {
|
||||
const now = new Date().toISOString()
|
||||
obj.createdAt = now
|
||||
obj.updatedAt = now
|
||||
})
|
||||
|
||||
db.topics.hook('updating', (modifications: any) => {
|
||||
if (typeof modifications === 'object') {
|
||||
modifications.updatedAt = new Date().toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
export default db
|
||||
|
||||
@ -36,3 +36,18 @@ export async function upgradeToV5(tx: Transaction): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function upgradeToV6(tx: Transaction): Promise<void> {
|
||||
const topics = await tx.table('topics').toArray()
|
||||
|
||||
// 为每个 topic 添加时间戳,兼容老数据,默认按照最新的时间戳来
|
||||
const now = new Date().toISOString()
|
||||
for (const topic of topics) {
|
||||
if (!topic.createdAt && !topic.updatedAt) {
|
||||
await tx.table('topics').update(topic.id, {
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { isLocalAi } from '@renderer/config/env'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { initializeMessagesState } from '@renderer/store/messages'
|
||||
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
@ -26,11 +25,6 @@ export function useAppInit() {
|
||||
|
||||
useFullScreenNotice()
|
||||
|
||||
// Initialize messages state
|
||||
useEffect(() => {
|
||||
dispatch(initializeMessagesState())
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
}, [avatar, dispatch])
|
||||
|
||||
168
src/renderer/src/hooks/useMessageOperations.ts
Normal file
168
src/renderer/src/hooks/useMessageOperations.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearStreamMessage,
|
||||
clearTopicMessages,
|
||||
commitStreamMessage,
|
||||
resendMessage,
|
||||
selectDisplayCount,
|
||||
selectTopicLoading,
|
||||
selectTopicMessages,
|
||||
setStreamMessage,
|
||||
updateMessage,
|
||||
updateMessages
|
||||
} from '@renderer/store/messages'
|
||||
import type { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { useCallback } from 'react'
|
||||
/**
|
||||
* 自定义Hook,提供消息操作相关的功能
|
||||
*
|
||||
* @param topic 当前主题
|
||||
* @returns 一组消息操作方法
|
||||
*/
|
||||
export function useMessageOperations(topic: Topic) {
|
||||
const dispatch = useAppDispatch()
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
|
||||
/**
|
||||
* 删除单个消息
|
||||
*/
|
||||
const deleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const newMessages = messages.filter((m) => m.id !== message.id)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
)
|
||||
|
||||
/**
|
||||
* 删除一组消息(基于askId)
|
||||
*/
|
||||
const deleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const newMessages = messages.filter((m) => m.askId !== askId)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
)
|
||||
|
||||
/**
|
||||
* 编辑消息内容
|
||||
*/
|
||||
const editMessage = useCallback(
|
||||
async (messageId: string, updates: Partial<Message>) => {
|
||||
await dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId,
|
||||
updates
|
||||
})
|
||||
)
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* 重新发送消息
|
||||
*/
|
||||
const resendMessageAction = useCallback(
|
||||
async (message: Message, assistant: Assistant, isMentionModel = false) => {
|
||||
return dispatch(resendMessage(message, assistant, topic, isMentionModel))
|
||||
},
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
* 重新发送用户消息(编辑后)
|
||||
*/
|
||||
const resendUserMessageWithEdit = useCallback(
|
||||
async (message: Message, editedContent: string, assistant: Assistant) => {
|
||||
// 先更新消息内容
|
||||
await editMessage(message.id, { content: editedContent })
|
||||
// 然后重新发送
|
||||
return dispatch(resendMessage({ ...message, content: editedContent }, assistant, topic))
|
||||
},
|
||||
[dispatch, editMessage, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
* 设置流式消息
|
||||
*/
|
||||
const setStreamMessageAction = useCallback(
|
||||
(message: Message | null) => {
|
||||
dispatch(setStreamMessage({ topicId: topic.id, message }))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* 提交流式消息
|
||||
*/
|
||||
const commitStreamMessageAction = useCallback(
|
||||
(messageId: string) => {
|
||||
dispatch(commitStreamMessage({ topicId: topic.id, messageId }))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* 清除流式消息
|
||||
*/
|
||||
const clearStreamMessageAction = useCallback(
|
||||
(messageId: string) => {
|
||||
dispatch(clearStreamMessage({ topicId: topic.id, messageId }))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* 清除会话消息
|
||||
*/
|
||||
const clearTopicMessagesAction = useCallback(
|
||||
async (_topicId?: string) => {
|
||||
await dispatch(clearTopicMessages(_topicId || topic.id))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* 更新消息数据
|
||||
*/
|
||||
const updateMessagesAction = useCallback(
|
||||
async (messages: Message[]) => {
|
||||
await dispatch(updateMessages(topic, messages))
|
||||
},
|
||||
[dispatch, topic]
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建新的上下文(clear message)
|
||||
*/
|
||||
const createNewContext = useCallback(async () => {
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||
}, [])
|
||||
|
||||
const loading = useAppSelector((state) => selectTopicLoading(state, topic.id))
|
||||
const displayCount = useAppSelector(selectDisplayCount)
|
||||
// /**
|
||||
// * 获取当前消息列表
|
||||
// */
|
||||
// const getMessages = useCallback(() => messages, [messages])
|
||||
|
||||
return {
|
||||
messages,
|
||||
loading,
|
||||
displayCount,
|
||||
updateMessages: updateMessagesAction,
|
||||
deleteMessage,
|
||||
deleteGroupMessages,
|
||||
editMessage,
|
||||
resendMessage: resendMessageAction,
|
||||
resendUserMessageWithEdit,
|
||||
setStreamMessage: setStreamMessageAction,
|
||||
commitStreamMessage: commitStreamMessageAction,
|
||||
clearStreamMessage: clearStreamMessageAction,
|
||||
createNewContext,
|
||||
clearTopicMessages: clearTopicMessagesAction
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import db from '@renderer/databases'
|
||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { prepareTopicMessages } from '@renderer/store/messages'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { find } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -15,6 +16,12 @@ export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
|
||||
|
||||
_activeTopic = activeTopic
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopic) {
|
||||
store.dispatch(prepareTopicMessages(activeTopic))
|
||||
}
|
||||
}, [activeTopic])
|
||||
|
||||
useEffect(() => {
|
||||
// activeTopic not in assistant.topics
|
||||
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
||||
@ -42,8 +49,16 @@ export async function getTopicById(topicId: string) {
|
||||
}
|
||||
|
||||
// Convert class to object with functions since class only has static methods
|
||||
// 只有静态方法,没必要用class
|
||||
// 只有静态方法,没必要用class,可以export {}
|
||||
export const TopicManager = {
|
||||
async getTopicLimit(limit: number) {
|
||||
return await db.topics
|
||||
.orderBy('updatedAt') // 按 updatedAt 排序(默认升序)
|
||||
.reverse() // 逆序(变成降序)
|
||||
.limit(limit) // 取前 10 条
|
||||
.toArray()
|
||||
},
|
||||
|
||||
async getTopic(id: string) {
|
||||
return await db.topics.get(id)
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
@ -24,7 +25,7 @@ import FileManager from '@renderer/services/FileManager'
|
||||
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store, { useAppDispatch } from '@renderer/store'
|
||||
import { sendMessage as _sendMessage } from '@renderer/store/messages'
|
||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||
import { Assistant, FileType, KnowledgeBase, MCPServer, Message, Model, Topic } from '@renderer/types'
|
||||
@ -59,7 +60,7 @@ interface Props {
|
||||
let _text = ''
|
||||
let _files: FileType[] = []
|
||||
|
||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
|
||||
@ -76,13 +77,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const [contextCount, setContextCount] = useState({ current: 0, max: 0 })
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
// const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
const [files, setFiles] = useState<FileType[]>(_files)
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef(null)
|
||||
const { searching } = useRuntime()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { loading } = useMessageOperations(topic)
|
||||
const dispatch = useAppDispatch()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
@ -131,9 +133,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
_files = files
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
|
||||
if (inputEmpty) {
|
||||
if (inputEmpty || loading) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
// Dispatch the sendMessage action with all options
|
||||
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||
dispatch(
|
||||
_sendMessage(text, assistant, assistant.topics[0], {
|
||||
_sendMessage(text, assistant, topic, {
|
||||
files: uploadedFiles,
|
||||
knowledgeBaseIds: selectedKnowledgeBases?.map((base) => base.id),
|
||||
mentionModels,
|
||||
@ -283,7 +283,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
|
||||
|
||||
const clearTopic = async () => {
|
||||
if (generating) {
|
||||
if (loading) {
|
||||
onPause()
|
||||
await delay(1)
|
||||
}
|
||||
@ -299,7 +299,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const onNewContext = () => {
|
||||
if (generating) return onPause()
|
||||
if (loading) return onPause()
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||
}
|
||||
|
||||
@ -478,7 +478,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}, [isDragging, handleDrag, handleDragEnd])
|
||||
|
||||
useShortcut('new_topic', () => {
|
||||
if (!generating) {
|
||||
if (!loading) {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
textareaRef.current?.focus()
|
||||
@ -742,14 +742,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
{generating && (
|
||||
{loading && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||
{!loading && <SendMessageButton sendMessage={sendMessage} disabled={loading || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
|
||||
@ -5,11 +5,8 @@ import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateMessages } from '@renderer/store/messages'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Divider, Dropdown } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -31,9 +28,7 @@ interface Props {
|
||||
style?: React.CSSProperties
|
||||
isGrouped?: boolean
|
||||
isStreaming?: boolean
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
}
|
||||
|
||||
const MessageItem: FC<Props> = ({
|
||||
@ -44,11 +39,8 @@ const MessageItem: FC<Props> = ({
|
||||
hidePresetMessages,
|
||||
isGrouped,
|
||||
isStreaming = false,
|
||||
style,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
style
|
||||
}) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||
@ -114,19 +106,6 @@ const MessageItem: FC<Props> = ({
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [message.id, messageHighlightHandler])
|
||||
|
||||
useEffect(() => {
|
||||
if (message.role === 'user' && !message.usage && topic) {
|
||||
runAsyncFunction(async () => {
|
||||
const usage = await estimateMessageUsage(message)
|
||||
if (topic) {
|
||||
await dispatch(
|
||||
updateMessages(topic, onGetMessages?.()?.map((m) => (m.id === message.id ? { ...m, usage } : m)) || [])
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [message, topic, dispatch, onGetMessages])
|
||||
|
||||
if (hidePresetMessages && message.isPreset) {
|
||||
return null
|
||||
}
|
||||
@ -187,8 +166,6 @@ const MessageItem: FC<Props> = ({
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef}
|
||||
setModel={setModel}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
|
||||
@ -6,7 +6,6 @@ import { classNames } from '@renderer/utils'
|
||||
import { Popover } from 'antd'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||
@ -16,23 +15,12 @@ interface Props {
|
||||
messages: (Message & { index: number })[]
|
||||
topic: Topic
|
||||
hidePresetMessages?: boolean
|
||||
onGetMessages: () => Message[]
|
||||
onSetMessages: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage: (message: Message) => Promise<void>
|
||||
onDeleteGroupMessages: (askId: string) => Promise<void>
|
||||
}
|
||||
|
||||
const MessageGroup = ({
|
||||
messages,
|
||||
topic,
|
||||
hidePresetMessages,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages,
|
||||
onDeleteGroupMessages
|
||||
}: Props) => {
|
||||
const MessageGroup = ({ messages, topic, hidePresetMessages, onSetMessages }: Props) => {
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
// const { t } = useTranslation()
|
||||
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
|
||||
@ -44,21 +32,9 @@ const MessageGroup = ({
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
const isGrid = multiModelMessageStyle === 'grid'
|
||||
|
||||
const handleDeleteGroup = useCallback(async () => {
|
||||
const askId = messages[0]?.askId
|
||||
if (!askId) return
|
||||
// const handleDeleteGroup = useCallback(async () => {
|
||||
|
||||
window.modal.confirm({
|
||||
title: t('message.group.delete.title'),
|
||||
content: t('message.group.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('common.delete'),
|
||||
onOk: () => onDeleteGroupMessages(askId)
|
||||
})
|
||||
}, [messages, onDeleteGroupMessages, t])
|
||||
// }, [messages, t, deleteGroupMessages])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
@ -76,9 +52,9 @@ const MessageGroup = ({
|
||||
style: {
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
|
||||
},
|
||||
onSetMessages,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
onSetMessages
|
||||
// onDeleteMessage,
|
||||
// onGetMessages
|
||||
}
|
||||
|
||||
const messageWrapper = (
|
||||
@ -124,8 +100,7 @@ const MessageGroup = ({
|
||||
topic,
|
||||
hidePresetMessages,
|
||||
onSetMessages,
|
||||
onDeleteMessage,
|
||||
onGetMessages,
|
||||
// onDeleteMessage,
|
||||
gridPopoverTrigger
|
||||
]
|
||||
)
|
||||
@ -149,7 +124,7 @@ const MessageGroup = ({
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
onDelete={handleDeleteGroup}
|
||||
topic={topic}
|
||||
/>
|
||||
)}
|
||||
</GroupContainer>
|
||||
|
||||
@ -6,8 +6,9 @@ import {
|
||||
NumberOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,7 +23,7 @@ interface Props {
|
||||
messages: Message[]
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
onDelete: () => void
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
const MessageGroupMenuBar: FC<Props> = ({
|
||||
@ -31,9 +32,26 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
messages,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
onDelete
|
||||
topic
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteGroupMessages } = useMessageOperations(topic)
|
||||
|
||||
const handleDeleteGroup = async () => {
|
||||
const askId = messages[0]?.askId
|
||||
if (!askId) return
|
||||
|
||||
window.modal.confirm({
|
||||
title: t('message.group.delete.title'),
|
||||
content: t('message.group.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('common.delete'),
|
||||
onOk: () => deleteGroupMessages(askId)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<GroupMenuBar $layout={multiModelMessageStyle} className="group-menu-bar">
|
||||
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||||
@ -71,7 +89,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
|
||||
onClick={onDelete}
|
||||
onClick={handleDeleteGroup}
|
||||
/>
|
||||
</GroupMenuBar>
|
||||
)
|
||||
|
||||
@ -15,19 +15,11 @@ import { UploadOutlined } from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearStreamMessage,
|
||||
commitStreamMessage,
|
||||
resendMessage,
|
||||
setStreamMessage,
|
||||
updateMessage
|
||||
} from '@renderer/store/messages'
|
||||
import { selectTopicMessages } from '@renderer/store/messages'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||
@ -43,7 +35,6 @@ import { isEmpty } from 'lodash'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
@ -55,30 +46,24 @@ interface Props {
|
||||
isAssistantMessage: boolean
|
||||
messageContainerRef: React.RefObject<HTMLDivElement>
|
||||
setModel: (model: Model) => void
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
|
||||
const MessageMenubar: FC<Props> = (props) => {
|
||||
const {
|
||||
message,
|
||||
index,
|
||||
isGrouped,
|
||||
isLastMessage,
|
||||
isAssistantMessage,
|
||||
assistant,
|
||||
topic,
|
||||
model,
|
||||
messageContainerRef,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
} = props
|
||||
const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } =
|
||||
props
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const assistantModel = assistant?.model
|
||||
const dispatch = useAppDispatch()
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
const {
|
||||
messages,
|
||||
editMessage,
|
||||
setStreamMessage,
|
||||
deleteMessage,
|
||||
resendMessage,
|
||||
commitStreamMessage,
|
||||
clearStreamMessage
|
||||
} = useMessageOperations(topic)
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
@ -109,21 +94,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
if (!isEmpty(groupdMessages)) {
|
||||
for (const assistantMessage of groupdMessages) {
|
||||
const _model = assistantMessage.model || assistantModel
|
||||
await dispatch(resendMessage({ ...assistantMessage, model: _model }, assistant, topic))
|
||||
await resendMessage({ ...assistantMessage, model: _model }, assistant)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await dispatch(resendMessage(messageUpdate ?? message, assistant, topic))
|
||||
await resendMessage(messageUpdate ?? message, assistant)
|
||||
},
|
||||
[message, assistantModel, model, onDeleteMessage, onGetMessages, dispatch, assistant, topic]
|
||||
[message, assistantModel, resendMessage, assistant]
|
||||
)
|
||||
|
||||
// const onResendUserMessage = useCallback(async () => {
|
||||
// // await dispatch(resendMessage(message, assistant, topic))
|
||||
// onResend()
|
||||
// }, [message, dispatch, assistant, topic])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
let resendMessage = false
|
||||
|
||||
@ -145,52 +125,41 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
})
|
||||
if (editedText && editedText !== message.content) {
|
||||
// 同步修改store中用户消息
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { content: editedText } }))
|
||||
editMessage(message.id, { content: editedText })
|
||||
|
||||
// const updatedMessages = onGetMessages?.() || []
|
||||
// dispatch(updateMessages(topic, updatedMessages))
|
||||
}
|
||||
|
||||
if (resendMessage) handleResendUserMessage({ ...message, content: editedText })
|
||||
}, [message, dispatch, topic, onGetMessages, handleResendUserMessage, t])
|
||||
}, [message, editMessage, topic, handleResendUserMessage, t])
|
||||
|
||||
const handleTranslate = useCallback(
|
||||
async (language: string) => {
|
||||
if (isTranslating) return
|
||||
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: message.id,
|
||||
updates: { translatedContent: t('translate.processing') }
|
||||
})
|
||||
)
|
||||
editMessage(message.id, { translatedContent: t('translate.processing') })
|
||||
|
||||
setIsTranslating(true)
|
||||
|
||||
try {
|
||||
await translateText(message.content, language, (text) => {
|
||||
// 使用 setStreamMessage 来更新翻译内容
|
||||
dispatch(
|
||||
setStreamMessage({
|
||||
topicId: topic.id,
|
||||
message: { ...message, translatedContent: text }
|
||||
})
|
||||
)
|
||||
setStreamMessage({ ...message, translatedContent: text })
|
||||
})
|
||||
|
||||
// 翻译完成后,提交流消息
|
||||
dispatch(commitStreamMessage({ topicId: topic.id, messageId: message.id }))
|
||||
commitStreamMessage(message.id)
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
window.message.error({ content: t('translate.error.failed'), key: 'translate-message' })
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { translatedContent: undefined } }))
|
||||
dispatch(clearStreamMessage({ topicId: topic.id, messageId: message.id }))
|
||||
editMessage(message.id, { translatedContent: undefined })
|
||||
clearStreamMessage(message.id)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
},
|
||||
[isTranslating, message, dispatch, topic, t]
|
||||
[isTranslating, message, editMessage, setStreamMessage, commitStreamMessage, clearStreamMessage, t]
|
||||
)
|
||||
|
||||
const dropdownItems = useMemo(
|
||||
@ -272,8 +241,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
await modelGenerating()
|
||||
const selectedModel = isGrouped ? model : assistantModel
|
||||
const _message = resetAssistantMessage(message, selectedModel)
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: _message }))
|
||||
dispatch(resendMessage(_message, assistant, topic))
|
||||
editMessage(message.id, { ..._message })
|
||||
resendMessage(_message, assistant)
|
||||
}
|
||||
|
||||
const onMentionModel = async (e: React.MouseEvent) => {
|
||||
@ -284,15 +253,15 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
// const mentionModelMessage: Message = resetAssistantMessage(message, selectedModel)
|
||||
// dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: _message }))
|
||||
await dispatch(resendMessage(message, { ...assistant, model: selectedModel }, topic, true))
|
||||
resendMessage(message, { ...assistant, model: selectedModel }, true)
|
||||
}
|
||||
|
||||
const onUseful = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { useful: !message.useful } }))
|
||||
editMessage(message.id, { useful: !message.useful })
|
||||
},
|
||||
[message, dispatch, topic]
|
||||
[message, editMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -350,14 +319,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () =>
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: message.id,
|
||||
updates: { translatedContent: undefined }
|
||||
})
|
||||
)
|
||||
onClick: () => editMessage(message.id, { translatedContent: undefined })
|
||||
}
|
||||
],
|
||||
onClick: (e) => e.domEvent.stopPropagation()
|
||||
@ -379,27 +341,19 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
disabled={isGrouped}
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={
|
||||
isGrouped
|
||||
? (e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteMessage?.(message)
|
||||
}
|
||||
: (e) => e.stopPropagation()
|
||||
}>
|
||||
<DeleteOutlined />
|
||||
{!isGrouped && (
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => deleteMessage(message)}>
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<DeleteOutlined />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { selectStreamMessage } from '@renderer/store/messages'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
@ -14,8 +15,6 @@ interface MessageStreamProps {
|
||||
isGrouped?: boolean
|
||||
style?: React.CSSProperties
|
||||
onSetMessages?: React.Dispatch<React.SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
|
||||
const MessageStreamContainer = styled.div`
|
||||
@ -32,9 +31,7 @@ const MessageStream: React.FC<MessageStreamProps> = ({
|
||||
hidePresetMessages,
|
||||
isGrouped,
|
||||
style,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages
|
||||
onSetMessages
|
||||
}) => {
|
||||
// 获取流式消息
|
||||
const streamMessage = useAppSelector((state) => selectStreamMessage(state, _message.topicId, _message.id))
|
||||
@ -55,8 +52,7 @@ const MessageStream: React.FC<MessageStreamProps> = ({
|
||||
// 在hooks调用后进行条件判断
|
||||
const isStreaming = !!(streamMessage && streamMessage.id === _message.id)
|
||||
const message = isStreaming ? streamMessage : regularMessage
|
||||
// console.log('streamMessage', streamMessage)
|
||||
// console.log('regularMessage', regularMessage)
|
||||
console.log('isStreaming', isStreaming)
|
||||
return (
|
||||
<MessageStreamContainer>
|
||||
<MessageItem
|
||||
@ -69,11 +65,9 @@ const MessageStream: React.FC<MessageStreamProps> = ({
|
||||
style={style}
|
||||
isStreaming={isStreaming}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageStreamContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageStream
|
||||
export default memo(MessageStream)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { getTopic } from '@renderer/hooks/useTopic'
|
||||
@ -9,14 +10,7 @@ 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 { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearTopicMessages,
|
||||
selectDisplayCount,
|
||||
selectLoading,
|
||||
selectTopicMessages,
|
||||
updateMessages
|
||||
} from '@renderer/store/messages'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import type { Assistant, Message, Topic } from '@renderer/types'
|
||||
import {
|
||||
captureScrollableDivAsBlob,
|
||||
@ -46,20 +40,12 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
const { t } = useTranslation()
|
||||
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
const messages = useAppSelector((state) => selectTopicMessages(state, topic.id))
|
||||
const loading = useAppSelector(selectLoading)
|
||||
const displayCount = useAppSelector(selectDisplayCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
|
||||
const messagesRef = useRef<Message[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
}, [messages])
|
||||
const { messages, loading, displayCount, updateMessages, clearTopicMessages } = useMessageOperations(topic)
|
||||
|
||||
useEffect(() => {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
@ -69,22 +55,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
setHasMore(messages.length > displayCount)
|
||||
}, [messages, displayCount])
|
||||
|
||||
const handleDeleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const newMessages = messages.filter((m) => m.id !== message.id)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
)
|
||||
|
||||
const handleDeleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const newMessages = messages.filter((m) => m.askId !== askId)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
)
|
||||
|
||||
const maxWidth = useMemo(() => {
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
@ -97,17 +67,16 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
}, [])
|
||||
|
||||
const autoRenameTopic = useCallback(async () => {
|
||||
let messages = [...messagesRef.current]
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
|
||||
if (isEmpty(messages)) {
|
||||
return
|
||||
}
|
||||
|
||||
messages = messages.filter((m) => m.status === 'success')
|
||||
const filteredMessages = messages.filter((m) => m.status === 'success')
|
||||
|
||||
if (!enableTopicNaming) {
|
||||
const topicName = messages[0]?.content.substring(0, 50)
|
||||
const topicName = filteredMessages[0]?.content.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ..._topic, name: topicName } as Topic
|
||||
setActiveTopic(data)
|
||||
@ -116,8 +85,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
return
|
||||
}
|
||||
|
||||
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (_topic && _topic.name === t('chat.default.topic.name') && filteredMessages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages: filteredMessages, assistant })
|
||||
if (summaryText) {
|
||||
const data = { ..._topic, name: summaryText }
|
||||
setActiveTopic(data)
|
||||
@ -125,11 +94,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assistant, topic.id, enableTopicNaming, t, setActiveTopic])
|
||||
}, [assistant, topic.id, enableTopicNaming, t, setActiveTopic, updateTopic, messages])
|
||||
|
||||
useEffect(() => {
|
||||
const messages = messagesRef.current
|
||||
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, () => {
|
||||
scrollToBottom()
|
||||
@ -138,12 +105,12 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
const defaultTopic = getDefaultTopic(assistant.id)
|
||||
|
||||
if (data && data.id !== topic.id) {
|
||||
await dispatch(clearTopicMessages(data.id))
|
||||
await clearTopicMessages(data.id)
|
||||
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
||||
return
|
||||
}
|
||||
|
||||
await dispatch(clearTopicMessages(topic.id))
|
||||
await clearTopicMessages()
|
||||
setDisplayMessages([])
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
if (_topic) {
|
||||
@ -166,7 +133,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, async () => {
|
||||
const lastMessage = last(messages)
|
||||
if (lastMessage?.type === 'clear') {
|
||||
handleDeleteMessage(lastMessage)
|
||||
// TODO
|
||||
// handleDeleteMessage(lastMessage)
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
@ -175,17 +143,25 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
|
||||
const clearMessage = getUserMessage({ assistant, topic, type: 'clear' })
|
||||
const newMessages = [...messages, clearMessage]
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
await updateMessages(newMessages)
|
||||
scrollToBottom()
|
||||
})
|
||||
]
|
||||
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant, dispatch, handleDeleteMessage, scrollToBottom, topic, updateTopic])
|
||||
return () => {
|
||||
for (const unsub of unsubscribes) {
|
||||
unsub()
|
||||
}
|
||||
}
|
||||
}, [assistant, dispatch, scrollToBottom, topic, updateTopic])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic)]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
return () => {
|
||||
for (const unsub of unsubscribes) {
|
||||
unsub()
|
||||
}
|
||||
}
|
||||
}, [autoRenameTopic])
|
||||
|
||||
useEffect(() => {
|
||||
@ -246,9 +222,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
topic={topic}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setDisplayMessages}
|
||||
onDeleteMessage={handleDeleteMessage}
|
||||
onDeleteGroupMessages={handleDeleteGroupMessages}
|
||||
onGetMessages={() => messages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
|
||||
@ -22,6 +22,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
|
||||
import {
|
||||
exportMarkdownToNotion,
|
||||
@ -35,7 +36,6 @@ import { findIndex } from 'lodash'
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { removeSpecialCharactersForFileName } from '@renderer/utils'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@ -117,7 +117,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
|
||||
const onSwitchTopic = useCallback(
|
||||
async (topic: Topic) => {
|
||||
await modelGenerating()
|
||||
// await modelGenerating()
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[setActiveTopic]
|
||||
|
||||
@ -136,8 +136,11 @@ export async function addAssistantMessagesToTopic({ assistant, topic }: { assist
|
||||
message.usage = await estimateMessageUsage(message)
|
||||
messages.push(message)
|
||||
}
|
||||
|
||||
db.topics.put({ id: topic.id, messages }, topic.id)
|
||||
if (await db.topics.get(topic.id)) {
|
||||
await db.topics.update(topic.id, { messages })
|
||||
} else {
|
||||
await db.topics.add({ id: topic.id, messages })
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
@ -1,24 +1,20 @@
|
||||
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { createAsyncThunk, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
import db from '@renderer/databases'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getAssistantMessage, getUserMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import type { AppDispatch, RootState } from '@renderer/store'
|
||||
import type { Assistant, FileType, MCPServer, Message, Model, Topic } from '@renderer/types'
|
||||
import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const convertToDBFormat = (messages: Message[]): Message[] => {
|
||||
return [...messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
}
|
||||
|
||||
export interface MessagesState {
|
||||
messagesByTopic: Record<string, Message[]>
|
||||
streamMessagesByTopic: Record<string, Record<string, Message | null>>
|
||||
currentTopic: string
|
||||
loading: boolean
|
||||
currentTopic: Topic | null
|
||||
loadingByTopic: Record<string, boolean> // 每个会话独立的loading状态
|
||||
displayCount: number
|
||||
error: string | null
|
||||
}
|
||||
@ -26,33 +22,69 @@ export interface MessagesState {
|
||||
const initialState: MessagesState = {
|
||||
messagesByTopic: {},
|
||||
streamMessagesByTopic: {},
|
||||
currentTopic: '',
|
||||
loading: false,
|
||||
currentTopic: null,
|
||||
loadingByTopic: {},
|
||||
displayCount: 20,
|
||||
error: null
|
||||
}
|
||||
|
||||
export const initializeMessagesState = createAsyncThunk('messages/initialize', async () => {
|
||||
// Get all topics from database
|
||||
const topics = await TopicManager.getAllTopics()
|
||||
const messagesByTopic: Record<string, Message[]> = {}
|
||||
// const MAX_RECENT_TOPICS = 10
|
||||
|
||||
// Group topics by assistantId and update messagesByTopic
|
||||
for (const topic of topics) {
|
||||
if (topic.messages && topic.messages.length > 0) {
|
||||
messagesByTopic[topic.id] = topic.messages.map((msg) => ({ ...msg }))
|
||||
// // 只初始化最近的会话消息
|
||||
// export const initializeMessagesState = createAsyncThunk('messages/initialize', async () => {
|
||||
// try {
|
||||
// // 获取所有会话的基本信息
|
||||
// const recentTopics = await TopicManager.getTopicLimit(MAX_RECENT_TOPICS)
|
||||
// console.log('recentTopics', recentTopics)
|
||||
// const messagesByTopic: Record<string, Message[]> = {}
|
||||
|
||||
// // 只加载最近会话的消息
|
||||
// for (const topic of recentTopics) {
|
||||
// if (topic.messages && topic.messages.length > 0) {
|
||||
// const messages = topic.messages.map((msg) => ({ ...msg }))
|
||||
// messagesByTopic[topic.id] = messages
|
||||
// }
|
||||
// }
|
||||
|
||||
// return messagesByTopic
|
||||
// } catch (error) {
|
||||
// console.error('Failed to initialize recent messages:', error)
|
||||
// return {}
|
||||
// }
|
||||
// })
|
||||
|
||||
// 新增准备会话消息的函数,实现懒加载机制
|
||||
export const prepareTopicMessages = createAsyncThunk(
|
||||
'messages/prepareTopic',
|
||||
async (topic: Topic, { dispatch, getState }) => {
|
||||
try {
|
||||
const state = getState() as RootState
|
||||
const hasMessageInStore = !!state.messages.messagesByTopic[topic.id]
|
||||
|
||||
// 如果消息不在 Redux store 中,从数据库加载
|
||||
if (!hasMessageInStore) {
|
||||
// 从数据库加载
|
||||
await loadTopicMessagesThunk(topic)(dispatch as AppDispatch)
|
||||
}
|
||||
|
||||
// 设置为当前会话
|
||||
dispatch(setCurrentTopic(topic))
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to prepare topic messages:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return messagesByTopic
|
||||
})
|
||||
)
|
||||
|
||||
const messagesSlice = createSlice({
|
||||
name: 'messages',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
setTopicLoading: (state, action: PayloadAction<{ topicId: string; loading: boolean }>) => {
|
||||
const { topicId, loading } = action.payload
|
||||
state.loadingByTopic[topicId] = loading
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload
|
||||
@ -70,6 +102,7 @@ const messagesSlice = createSlice({
|
||||
// 不是什么好主意,不符合语义
|
||||
state.messagesByTopic[topicId].push(...messages)
|
||||
} else {
|
||||
// 添加单条消息
|
||||
state.messagesByTopic[topicId].push(messages)
|
||||
}
|
||||
},
|
||||
@ -80,13 +113,13 @@ const messagesSlice = createSlice({
|
||||
const { topicId, messageId, updates } = action.payload
|
||||
const topicMessages = state.messagesByTopic[topicId]
|
||||
if (topicMessages) {
|
||||
const messageIndex = topicMessages.findIndex((msg) => msg.id === messageId)
|
||||
if (messageIndex !== -1) {
|
||||
topicMessages[messageIndex] = { ...topicMessages[messageIndex], ...updates }
|
||||
const message = topicMessages.find((msg) => msg.id === messageId)
|
||||
if (message) {
|
||||
Object.assign(message, updates)
|
||||
}
|
||||
}
|
||||
},
|
||||
setCurrentTopic: (state, action: PayloadAction<string>) => {
|
||||
setCurrentTopic: (state, action: PayloadAction<Topic | null>) => {
|
||||
state.currentTopic = action.payload
|
||||
},
|
||||
clearTopicMessages: (state, action: PayloadAction<string>) => {
|
||||
@ -96,7 +129,7 @@ const messagesSlice = createSlice({
|
||||
},
|
||||
loadTopicMessages: (state, action: PayloadAction<{ topicId: string; messages: Message[] }>) => {
|
||||
const { topicId, messages } = action.payload
|
||||
state.messagesByTopic[topicId] = messages.map((msg) => ({ ...msg }))
|
||||
state.messagesByTopic[topicId] = messages
|
||||
},
|
||||
setStreamMessage: (state, action: PayloadAction<{ topicId: string; message: Message | null }>) => {
|
||||
const { topicId, message } = action.payload
|
||||
@ -111,24 +144,29 @@ const messagesSlice = createSlice({
|
||||
const { topicId, messageId } = action.payload
|
||||
const streamMessage = state.streamMessagesByTopic[topicId]?.[messageId]
|
||||
|
||||
// 如果没有流消息,则不执行任何操作
|
||||
// 如果没有流消息或不是助手消息,则跳过
|
||||
if (!streamMessage || streamMessage.role !== 'assistant') {
|
||||
return
|
||||
}
|
||||
|
||||
// 查找是否已经存在具有相同Id的助手消息
|
||||
const existingMessageIndex =
|
||||
state.messagesByTopic[topicId]?.findIndex((m) => m.role === 'assistant' && m.id === streamMessage.id) ?? -1
|
||||
|
||||
if (existingMessageIndex !== -1) {
|
||||
// 替换已有的消息
|
||||
state.messagesByTopic[topicId][existingMessageIndex] = streamMessage
|
||||
} else if (state.messagesByTopic[topicId]) {
|
||||
// 如果不存在但存在topicMessages,则添加新消息
|
||||
state.messagesByTopic[topicId].push(streamMessage)
|
||||
// 确保消息数组存在
|
||||
if (!state.messagesByTopic[topicId]) {
|
||||
state.messagesByTopic[topicId] = []
|
||||
}
|
||||
|
||||
// 只删除这个特定消息的流状态
|
||||
// 尝试找到现有消息
|
||||
const existingMessage = state.messagesByTopic[topicId].find(
|
||||
(m) => m.role === 'assistant' && m.id === streamMessage.id
|
||||
)
|
||||
|
||||
if (existingMessage) {
|
||||
// 更新
|
||||
Object.assign(existingMessage, streamMessage)
|
||||
} else {
|
||||
// 添加新消息
|
||||
state.messagesByTopic[topicId].push(streamMessage)
|
||||
}
|
||||
// 删除流状态
|
||||
delete state.streamMessagesByTopic[topicId][messageId]
|
||||
},
|
||||
clearStreamMessage: (state, action: PayloadAction<{ topicId: string; messageId: string }>) => {
|
||||
@ -137,27 +175,24 @@ const messagesSlice = createSlice({
|
||||
delete state.streamMessagesByTopic[topicId][messageId]
|
||||
}
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(initializeMessagesState.pending, (state) => {
|
||||
state.loading = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(initializeMessagesState.fulfilled, (state, action) => {
|
||||
console.log('initializeMessagesState.fulfilled', action.payload)
|
||||
state.loading = false
|
||||
state.messagesByTopic = action.payload
|
||||
})
|
||||
.addCase(initializeMessagesState.rejected, (state, action) => {
|
||||
state.loading = false
|
||||
state.error = action.error.message || 'Failed to load messages'
|
||||
})
|
||||
}
|
||||
// extraReducers: (builder) => {
|
||||
// builder
|
||||
// .addCase(initializeMessagesState.pending, (state) => {
|
||||
// state.error = null
|
||||
// })
|
||||
// .addCase(initializeMessagesState.fulfilled, (state, action) => {
|
||||
// console.log('initializeMessagesState.fulfilled', action.payload)
|
||||
// state.messagesByTopic = action.payload
|
||||
// })
|
||||
// .addCase(initializeMessagesState.rejected, (state, action) => {
|
||||
// state.error = action.error.message || 'Failed to load messages'
|
||||
// })
|
||||
// }
|
||||
})
|
||||
|
||||
export const {
|
||||
setLoading,
|
||||
setTopicLoading,
|
||||
setError,
|
||||
setDisplayCount,
|
||||
addMessage,
|
||||
@ -178,24 +213,28 @@ const handleResponseMessageUpdate = (message, topicId, dispatch, getState) => {
|
||||
if (message.status === 'success') {
|
||||
EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME)
|
||||
}
|
||||
|
||||
dispatch(commitStreamMessage({ topicId, messageId: message.id }))
|
||||
|
||||
const state = getState()
|
||||
const topicMessages = state.messages.messagesByTopic[topicId]
|
||||
if (topicMessages) {
|
||||
syncMessagesWithDB(topicId, topicMessages)
|
||||
if (message.status !== 'sending') {
|
||||
dispatch(commitStreamMessage({ topicId, messageId: message.id }))
|
||||
const state = getState()
|
||||
const topicMessages = state.messages.messagesByTopic[topicId]
|
||||
if (topicMessages) {
|
||||
syncMessagesWithDB(topicId, topicMessages)
|
||||
}
|
||||
dispatch(setTopicLoading({ topicId, loading: false }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to sync messages with database
|
||||
const syncMessagesWithDB = async (topicId: string, messages: Message[]) => {
|
||||
const dbMessages = convertToDBFormat(messages)
|
||||
await db.topics.put({
|
||||
id: topicId,
|
||||
messages: dbMessages
|
||||
})
|
||||
const topic = await db.topics.get(topicId)
|
||||
if (topic) {
|
||||
await db.topics.update(topicId, {
|
||||
messages
|
||||
})
|
||||
} else {
|
||||
await db.topics.add({ id: topicId, messages })
|
||||
}
|
||||
}
|
||||
|
||||
// Modified sendMessage thunk
|
||||
@ -215,7 +254,7 @@ export const sendMessage =
|
||||
) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
|
||||
|
||||
// Initialize topic messages if not exists
|
||||
const initialState = getState()
|
||||
@ -229,11 +268,10 @@ export const sendMessage =
|
||||
// 使用用户消息
|
||||
let userMessage: Message
|
||||
if (isResend) {
|
||||
userMessage = options.resendUserMessage!
|
||||
userMessage = options.resendUserMessage
|
||||
} else {
|
||||
// 创建新的用户消息
|
||||
userMessage = getUserMessage({ assistant, topic, type: 'text', content })
|
||||
|
||||
if (options?.files) {
|
||||
userMessage.files = options.files
|
||||
}
|
||||
@ -249,11 +287,7 @@ export const sendMessage =
|
||||
if (options?.enabledMCPs) {
|
||||
userMessage.enabledMCPs = options.enabledMCPs
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是重发,才添加新的用户消息
|
||||
if (!isResend) {
|
||||
dispatch(addMessage({ topicId: topic.id, messages: userMessage }))
|
||||
userMessage.usage = await estimateMessageUsage(userMessage)
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
||||
@ -261,7 +295,6 @@ export const sendMessage =
|
||||
// 处理助手消息
|
||||
// let assistantMessage: Message
|
||||
let assistantMessages: Message[] = []
|
||||
|
||||
// 使用助手消息
|
||||
if (isResend && options.resendAssistantMessage) {
|
||||
// 直接使用传入的助手消息,进行重置
|
||||
@ -270,7 +303,6 @@ export const sendMessage =
|
||||
const resetMessage = resetAssistantMessage(messageToReset, model)
|
||||
// 更新状态
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: id, updates: resetMessage }))
|
||||
|
||||
// 使用重置后的消息
|
||||
assistantMessages.push(resetMessage)
|
||||
} else {
|
||||
@ -293,17 +325,18 @@ export const sendMessage =
|
||||
}
|
||||
}
|
||||
|
||||
// Use topic queue to handle request
|
||||
// 如果不是重发
|
||||
!options?.resendAssistantMessage &&
|
||||
dispatch(
|
||||
addMessage({
|
||||
topicId: topic.id,
|
||||
messages: !isResend ? [userMessage, ...assistantMessages] : assistantMessages
|
||||
})
|
||||
)
|
||||
|
||||
const queue = getTopicQueue(topic.id)
|
||||
|
||||
// let assistantMessage: Message | undefined
|
||||
if (!isResend) {
|
||||
dispatch(addMessage({ topicId: topic.id, messages: assistantMessages }))
|
||||
}
|
||||
|
||||
for (const assistantMessage of assistantMessages) {
|
||||
// console.log('assistantMessage', assistantMessage)
|
||||
|
||||
// Set as stream message instead of adding to messages
|
||||
dispatch(setStreamMessage({ topicId: topic.id, message: assistantMessage }))
|
||||
|
||||
@ -314,18 +347,16 @@ export const sendMessage =
|
||||
if (currentTopicMessages) {
|
||||
await syncMessagesWithDB(topic.id, currentTopicMessages)
|
||||
}
|
||||
|
||||
// 保证请求有序,防止请求静态,限制并发数量
|
||||
queue.add(async () => {
|
||||
try {
|
||||
const state = getState()
|
||||
const topicMessages = state.messages.messagesByTopic[topic.id]
|
||||
if (!topicMessages) {
|
||||
const messages = state.messages.messagesByTopic[topic.id]
|
||||
if (!messages) {
|
||||
dispatch(clearTopicMessages(topic.id))
|
||||
return
|
||||
}
|
||||
|
||||
const messages = convertToDBFormat(topicMessages)
|
||||
|
||||
// Prepare assistant config
|
||||
const assistantWithModel = assistantMessage.model
|
||||
? { ...assistant, model: assistantMessage.model }
|
||||
@ -374,8 +405,7 @@ export const sendMessage =
|
||||
} catch (error: any) {
|
||||
console.error('Error in sendMessage:', error)
|
||||
dispatch(setError(error.message))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +424,7 @@ export const resendMessage =
|
||||
// 查找此用户消息对应的助手消息
|
||||
const assistantMessage = topicMessages.find((m) => m.role === 'assistant' && m.askId === message.id)
|
||||
|
||||
dispatch(
|
||||
return dispatch(
|
||||
sendMessage(message.content, assistant, topic, {
|
||||
resendUserMessage: message,
|
||||
resendAssistantMessage: assistantMessage
|
||||
@ -404,11 +434,10 @@ export const resendMessage =
|
||||
|
||||
// 如果是助手消息,找到对应的用户消息
|
||||
const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user')
|
||||
|
||||
console.log('topicMessages,topicMessages', topicMessages)
|
||||
if (!userMessage) {
|
||||
console.error('Cannot find original user message to resend')
|
||||
dispatch(setError('Cannot find original user message to resend'))
|
||||
return
|
||||
return dispatch(setError('Cannot find original user message to resend'))
|
||||
}
|
||||
|
||||
if (isMentionModel) {
|
||||
@ -430,32 +459,44 @@ export const resendMessage =
|
||||
console.error('Error in resendMessage:', error)
|
||||
dispatch(setError(error.message))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified loadTopicMessages thunk
|
||||
export const loadTopicMessagesThunk = (topicId: string) => async (dispatch: AppDispatch) => {
|
||||
export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
const topic = await db.topics.get(topicId)
|
||||
const messages = topic?.messages || []
|
||||
|
||||
// Initialize topic messages
|
||||
dispatch(clearTopicMessages(topicId))
|
||||
dispatch(loadTopicMessages({ topicId, messages }))
|
||||
dispatch(setCurrentTopic(topicId))
|
||||
// 设置会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
|
||||
dispatch(setCurrentTopic(topic))
|
||||
try {
|
||||
// 使用 getTopic 获取会话对象
|
||||
const topicWithDB = await TopicManager.getTopic(topic.id)
|
||||
if (topicWithDB) {
|
||||
// 如果数据库中有会话,加载消息,保存会话
|
||||
dispatch(loadTopicMessages({ topicId: topic.id, messages: topicWithDB.messages }))
|
||||
}
|
||||
// else {
|
||||
// // 如果找不到,可以将当前会话设为 null
|
||||
// dispatch(setCurrentTopic(null))
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('Failed to get complete topic:', error)
|
||||
dispatch(setCurrentTopic(null))
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
// 清除会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified clearMessages thunk
|
||||
export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
// 设置会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
|
||||
|
||||
// Wait for any pending requests to complete
|
||||
await waitForTopicQueue(topic.id)
|
||||
@ -468,49 +509,50 @@ export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDis
|
||||
await db.topics.update(topic.id, { messages: [] })
|
||||
|
||||
// Update current topic
|
||||
dispatch(setCurrentTopic(topic.id))
|
||||
dispatch(setCurrentTopic(topic))
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to clear messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
// 清除会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified updateMessages thunk
|
||||
// 修改的 updateMessages thunk,同时更新缓存
|
||||
export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
// 设置会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
|
||||
|
||||
// 更新数据库
|
||||
await db.topics.update(topic.id, { messages })
|
||||
|
||||
// 更新 Redux store
|
||||
dispatch(loadTopicMessages({ topicId: topic.id, messages }))
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to update messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
// 清除会话的loading状态
|
||||
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Selectors
|
||||
export const selectTopicMessages = createSelector(
|
||||
[(state: RootState) => state.messages, (_, topicId: string) => topicId],
|
||||
(messagesState, topicId) => {
|
||||
const topicMessages = messagesState.messagesByTopic[topicId]
|
||||
|
||||
if (!topicMessages) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [...topicMessages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
}
|
||||
)
|
||||
|
||||
export const selectCurrentTopicId = (state: RootState): string => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
return messagesState?.currentTopic || ''
|
||||
export const selectCurrentTopicId = (state: RootState): string | null => {
|
||||
const messagesState = state.messages
|
||||
return messagesState.currentTopic?.id ?? null
|
||||
}
|
||||
|
||||
export const selectLoading = (state: RootState): boolean => {
|
||||
export const selectTopicMessages = createSelector(
|
||||
[(state: RootState) => state.messages.messagesByTopic, (_, topicId: string) => topicId],
|
||||
(messagesByTopic, topicId) => (topicId ? (messagesByTopic[topicId] ?? []) : [])
|
||||
)
|
||||
|
||||
// 获取特定话题的loading状态
|
||||
export const selectTopicLoading = (state: RootState, topicId?: string): boolean => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
return messagesState?.loading || false
|
||||
const currentTopicId = topicId || messagesState.currentTopic?.id || ''
|
||||
return currentTopicId ? (messagesState.loadingByTopic[currentTopicId] ?? false) : false
|
||||
}
|
||||
|
||||
export const selectDisplayCount = (state: RootState): number => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user