refactor: 重构message模块 (#2561)
* feat: Implement Redux-based message management with enhanced state handling - Add new Redux slice for managing messages with advanced state control - Introduce topic-specific message queues using p-queue for request management - Refactor message sending, loading, and updating logic - Improve error handling and state synchronization with database - Add selectors for efficient message retrieval and state access * feat: Implement streaming message handling in Redux store - Add stream message support in messages slice - Create MessageStream component for rendering streaming messages - Update Inputbar and Suggestions components to use new Redux message sending logic - Refactor message sending flow to use stream message management - Improve error handling and message state management * feat:添加StreamMessage,优化数据流展示,减少大面积rerender * refactor: Simplify messages state management and initialization - Refactor messages slice to use flat message array instead of separate user/assistant messages - Add initializeMessagesState thunk to load messages from database on app startup - Update message-related reducers to work with flat message array - Modify MessageStream and related components to use new state structure - Improve type safety and reduce complexity in messages state management * ✨ feat: add Model Context Protocol (MCP) support (#2809) * ✨ feat: add Model Context Protocol (MCP) server configuration (main) - Added `@modelcontextprotocol/sdk` dependency for MCP integration. - Introduced MCP server configuration UI in settings with add, edit, delete, and activation functionalities. - Created `useMCPServers` hook to manage MCP server state and actions. - Added i18n support for MCP settings with translation keys. - Integrated MCP settings into the application's settings navigation and routing. - Implemented Redux state management for MCP servers. - Updated `yarn.lock` with new dependencies and their resolutions. * 🌟 feat: implement mcp service and integrate with ipc handlers - Added `MCPService` class to manage Model Context Protocol servers. - Implemented various handlers in `ipc.ts` for managing MCP servers including listing, adding, updating, deleting, and activating/deactivating servers. - Integrated MCP related types into existing type declarations for consistency across the application. - Updated `preload` to expose new MCP related APIs to the renderer process. - Enhanced `MCPSettings` component to interact directly with the new MCP service for adding, updating, deleting servers and setting their active states. - Introduced selectors in the MCP Redux slice for fetching active and all servers from the store. - Moved MCP types to a centralized location in `@renderer/types` for reuse across different parts of the application. * feat: enhance MCPService initialization to prevent recursive calls and improve error handling * feat: enhance MCP integration by adding MCPTool type and updating related methods * feat: implement streaming support for tool calls in OpenAIProvider and enhance message processing * refactor: Improve message handling and type safety in message components - Update Message, MessageGroup, MessageStream, and MessageMenubar to require Topic prop - Modify message sending and resending logic in MessageMenubar - Remove commented-out code and simplify message state management - Enhance type safety by explicitly defining prop types * fix: finish_reason undefined * refactor: Streamline message resending and state management - Introduce `resendMessage` thunk for more robust message resending logic - Update `sendMessage` to support resending existing messages - Remove deprecated `onEditMessage` callback from MessageMenubar - Simplify message state updates using Redux actions - Improve type safety and reduce complexity in message handling * refactor: Optimize message resending and event handling - Remove deprecated `APPEND_MESSAGE` event and related callbacks - Update `resendMessage` thunk to support mentioning new models - Modify message state synchronization in database - Simplify dependency arrays and remove unused event listeners - Add migration step for initializing messages state * refactor: Enhance message translation and suggestions handling - Update MessageMenubar to use stream message actions for translation - Modify Suggestions component to optimize suggestion fetching - Remove deprecated event listeners and simplify component logic - Memoize MessageMenubar and Suggestions components for performance - Trigger AI auto-rename on message completion in messages slice * refactor: Optimize message streaming with throttled updates - Introduce throttled message update mechanism using lodash - Improve performance by limiting Redux state updates during streaming - Create a separate handler for response message updates - Enhance message synchronization with database - Prevent unnecessary re-renders and reduce computational overhead * fix: Remove unnecessary await in message dispatch Removes the `await` keyword from the message dispatch in Inputbar, which was causing an unnecessary async operation. Also adds a missing closing brace in the migration configuration file. * fix: Update Redux persist configuration for messages slice Modify store configuration to exclude 'messages' slice from persistence and remove unnecessary migration step for message state initialization * feat: Enhance message streaming and multi-model support - Refactor Redux messages slice to support multiple stream messages per topic - Update MessageStream and messages slice to handle message streaming with message-specific IDs - Implement support for multi-model message generation - Modify queue concurrency to improve parallel message processing - Update message selection and streaming logic to be more flexible and robust * feat: Implement file upload handling in message sending - Add FileManager service integration for file uploads in Inputbar - Modify sendMessage action to use uploaded file references - Update messages slice to conditionally dispatch messages during resend * ✨ feat(MCP): add support for enabling/disabling MCPServers per message (#2989) * ✨ feat: add MCP servers in chat input - Introduce MCPToolsButton component for managing MCP servers - Add new icon for MCP server tools in iconfont.css - Update Inputbar to include MCP tools functionality - Add toggle functionality for enabling/disabling MCP servers - Implement styled dropdown menu for server selection - Add necessary type imports and useState for MCP server management * ✨ feat: add support for enabling/disabling MCPServers per message (main) - Added `enabledMCPs` property to the `Message` type to track enabled MCPServers. - Modified `MCPToolsButton` to enable all active MCPServers by default using a new `enableAll` state. - Introduced `filterMCPTools` utility to filter tools based on enabled MCPServers. - Updated `AnthropicProvider`, `GeminiProvider`, and `OpenAIProvider` to filter tools using `filterMCPTools`. - Enhanced `Inputbar` to include `enabledMCPs` in the message payload when set. * ✨ feat(MCP): add enabledMCPs parameter to sendMessage action - Update sendMessage action type to include optional enabledMCPs parameter - Import MCPServer type for type safety - Modify action signature to support passing enabled MCP servers per message --------- Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com> Co-authored-by: lizhixuan <zhixuanli219643@sohu-inc.com> Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com> Co-authored-by: kangfenmao <kangfenmao@qq.com>
This commit is contained in:
parent
4a06c86412
commit
9afc6989af
@ -21,7 +21,8 @@ export default defineConfig({
|
||||
'@llm-tools/embedjs-loader-pdf',
|
||||
'@llm-tools/embedjs-loader-sitemap',
|
||||
'@llm-tools/embedjs-libsql',
|
||||
'@llm-tools/embedjs-loader-image'
|
||||
'@llm-tools/embedjs-loader-image',
|
||||
'p-queue'
|
||||
]
|
||||
}),
|
||||
...visualizerPlugin('main')
|
||||
|
||||
@ -81,6 +81,7 @@
|
||||
"fs-extra": "^11.2.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"p-queue": "^8.1.0",
|
||||
"tokenx": "^0.4.1",
|
||||
"webdav": "4.11.4"
|
||||
},
|
||||
|
||||
@ -12,3 +12,7 @@ export const isWindows = platform === 'win32' || platform === 'win64'
|
||||
export const isLinux = platform === 'linux'
|
||||
|
||||
export const SILICON_CLIENT_ID = 'SFaJLLq0y6CAMoyDm81aMu'
|
||||
|
||||
// Messages loading configuration
|
||||
export const INITIAL_MESSAGES_COUNT = 20
|
||||
export const LOAD_MORE_COUNT = 20
|
||||
|
||||
@ -3,6 +3,7 @@ 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'
|
||||
@ -25,6 +26,11 @@ export function useAppInit() {
|
||||
|
||||
useFullScreenNotice()
|
||||
|
||||
// Initialize messages state
|
||||
useEffect(() => {
|
||||
dispatch(initializeMessagesState())
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
}, [avatar, dispatch])
|
||||
|
||||
@ -41,28 +41,34 @@ export async function getTopicById(topicId: string) {
|
||||
return { ...topic, messages } as Topic
|
||||
}
|
||||
|
||||
export class TopicManager {
|
||||
static async getTopic(id: string) {
|
||||
// Convert class to object with functions since class only has static methods
|
||||
// 只有静态方法,没必要用class
|
||||
export const TopicManager = {
|
||||
async getTopic(id: string) {
|
||||
return await db.topics.get(id)
|
||||
}
|
||||
},
|
||||
|
||||
static async getTopicMessages(id: string) {
|
||||
const topic = await this.getTopic(id)
|
||||
async getAllTopics() {
|
||||
return await db.topics.toArray()
|
||||
},
|
||||
|
||||
async getTopicMessages(id: string) {
|
||||
const topic = await TopicManager.getTopic(id)
|
||||
return topic ? topic.messages : []
|
||||
}
|
||||
},
|
||||
|
||||
static async removeTopic(id: string) {
|
||||
const messages = await this.getTopicMessages(id)
|
||||
async removeTopic(id: string) {
|
||||
const messages = await TopicManager.getTopicMessages(id)
|
||||
|
||||
for (const message of messages) {
|
||||
await deleteMessageFiles(message)
|
||||
}
|
||||
|
||||
db.topics.delete(id)
|
||||
}
|
||||
},
|
||||
|
||||
static async clearTopicMessages(id: string) {
|
||||
const topic = await this.getTopic(id)
|
||||
async clearTopicMessages(id: string) {
|
||||
const topic = await TopicManager.getTopic(id)
|
||||
|
||||
if (topic) {
|
||||
for (const message of topic?.messages ?? []) {
|
||||
|
||||
@ -31,7 +31,7 @@ const Chat: FC<Props> = (props) => {
|
||||
topic={props.activeTopic}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
/>
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
</Main>
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<Tabs
|
||||
|
||||
@ -25,15 +25,15 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import store, { useAppDispatch, useAppSelector } 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'
|
||||
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { Assistant, FileType, KnowledgeBase, MCPServer, MCPServer, Message, Model, Topic } from '@renderer/types'
|
||||
import { classNames, delay, getFileExtension } from '@renderer/utils'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
import Logger from 'electron-log/renderer'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -50,10 +50,10 @@ import MentionModelsButton from './MentionModelsButton'
|
||||
import MentionModelsInput from './MentionModelsInput'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
let _text = ''
|
||||
@ -137,43 +137,28 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
return
|
||||
}
|
||||
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: assistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
type: 'text',
|
||||
status: 'success'
|
||||
try {
|
||||
// Dispatch the sendMessage action with all options
|
||||
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||
dispatch(
|
||||
_sendMessage(text, assistant, assistant.topics[0], {
|
||||
files: uploadedFiles,
|
||||
knowledgeBaseIds: selectedKnowledgeBases?.map((base) => base.id),
|
||||
mentionModels,
|
||||
enabledMCPs
|
||||
})
|
||||
)
|
||||
|
||||
// Clear input
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
setExpend(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error)
|
||||
}
|
||||
|
||||
if (selectedKnowledgeBases) {
|
||||
message.knowledgeBaseIds = selectedKnowledgeBases.map((base) => base.id)
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
message.files = await FileManager.uploadFiles(files)
|
||||
}
|
||||
|
||||
if (mentionModels.length > 0) {
|
||||
message.mentions = mentionModels
|
||||
}
|
||||
|
||||
if (enabledMCPs.length > 0) {
|
||||
message.enabledMCPs = enabledMCPs
|
||||
}
|
||||
|
||||
currentMessageId.current = message.id
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBases, files, mentionModels])
|
||||
}, [inputEmpty, text, assistant, files, selectedKnowledgeBases, mentionModels, dispatch])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTopic } from '@renderer/hooks/useTopic'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getContextCount, getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
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 { Divider, Dropdown } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -24,44 +23,46 @@ import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
topic?: Topic
|
||||
topic: Topic
|
||||
assistant?: Assistant
|
||||
index?: number
|
||||
total?: number
|
||||
hidePresetMessages?: boolean
|
||||
style?: React.CSSProperties
|
||||
isGrouped?: boolean
|
||||
isStreaming?: boolean
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
}
|
||||
|
||||
const MessageItem: FC<Props> = ({
|
||||
message: _message,
|
||||
topic: _topic,
|
||||
message,
|
||||
topic,
|
||||
// assistant,
|
||||
index,
|
||||
hidePresetMessages,
|
||||
isGrouped,
|
||||
isStreaming = false,
|
||||
style,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages
|
||||
}) => {
|
||||
const [message, setMessage] = useState(_message)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const topic = useTopic(assistant, _topic?.id)
|
||||
// const topic = useTopic(assistant, _topic?.id)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
|
||||
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
|
||||
const [selectedText, setSelectedText] = useState<string>('')
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
|
||||
const showMenubar = !message.status.includes('ing')
|
||||
const showMenubar = !isStreaming && !message.status.includes('ing')
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||
@ -95,28 +96,7 @@ const MessageItem: FC<Props> = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onEditMessage = useCallback(
|
||||
async (msg: Message) => {
|
||||
if (msg.role === 'user') {
|
||||
const usage = await estimateMessageUsage(msg)
|
||||
msg.usage = usage
|
||||
}
|
||||
|
||||
setMessage(msg)
|
||||
const messages = onGetMessages?.()?.map((m) => (m.id === message.id ? msg : m))
|
||||
messages && onSetMessages?.(messages)
|
||||
topic && db.topics.update(topic.id, { messages })
|
||||
|
||||
if (messages) {
|
||||
const tokensCount = await estimateHistoryTokens(assistant, messages)
|
||||
const contextCount = getContextCount(assistant, messages)
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, { tokensCount, contextCount })
|
||||
}
|
||||
},
|
||||
[message.id, onGetMessages, onSetMessages, topic, assistant]
|
||||
)
|
||||
|
||||
const messageHighlightHandler = (highlight: boolean = true) => {
|
||||
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
|
||||
if (messageContainerRef.current) {
|
||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
if (highlight) {
|
||||
@ -127,62 +107,25 @@ const MessageItem: FC<Props> = ({
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler),
|
||||
EventEmitter.on(EVENT_NAMES.RESEND_MESSAGE + ':' + message.id, onEditMessage)
|
||||
]
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [message, onEditMessage])
|
||||
}, [message.id, messageHighlightHandler])
|
||||
|
||||
useEffect(() => {
|
||||
if (message.role === 'user' && !message.usage) {
|
||||
if (message.role === 'user' && !message.usage && topic) {
|
||||
runAsyncFunction(async () => {
|
||||
const usage = await estimateMessageUsage(message)
|
||||
setMessage({ ...message, usage })
|
||||
const topic = await db.topics.get({ id: message.topicId })
|
||||
const messages = topic?.messages.map((m) => (m.id === message.id ? { ...m, usage } : m))
|
||||
db.topics.update(message.topicId, { messages })
|
||||
if (topic) {
|
||||
await dispatch(
|
||||
updateMessages(topic, onGetMessages?.()?.map((m) => (m.id === message.id ? { ...m, usage } : m)) || [])
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [message])
|
||||
|
||||
useEffect(() => {
|
||||
if (topic && onGetMessages && onSetMessages) {
|
||||
if (message.status === 'sending') {
|
||||
const messages = onGetMessages()
|
||||
const assistantWithModel = message.model ? { ...assistant, model: message.model } : assistant
|
||||
|
||||
if (topic.prompt) {
|
||||
assistantWithModel.prompt = assistantWithModel.prompt
|
||||
? `${assistantWithModel.prompt}\n${topic.prompt}`
|
||||
: topic.prompt
|
||||
}
|
||||
|
||||
fetchChatCompletion({
|
||||
message,
|
||||
messages: messages
|
||||
.filter((m) => !m.status.includes('ing'))
|
||||
.slice(
|
||||
0,
|
||||
messages.findIndex((m) => m.id === message.id)
|
||||
),
|
||||
assistant: assistantWithModel,
|
||||
onResponse: (msg) => {
|
||||
setMessage(msg)
|
||||
if (msg.status !== 'pending') {
|
||||
const _messages = onGetMessages().map((m) => (m.id === msg.id ? msg : m))
|
||||
onSetMessages(_messages)
|
||||
db.topics.update(topic.id, { messages: _messages })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [message.status])
|
||||
}, [message, topic, dispatch, onGetMessages])
|
||||
|
||||
if (hidePresetMessages && message.isPreset) {
|
||||
return null
|
||||
@ -235,15 +178,15 @@ const MessageItem: FC<Props> = ({
|
||||
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistantModel={assistant?.model}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
index={index}
|
||||
topic={topic}
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef}
|
||||
setModel={setModel}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import type { Message, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Popover } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
|
||||
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 MessageItem from './Message'
|
||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||
import MessageStream from './MessageStream'
|
||||
|
||||
interface Props {
|
||||
messages: (Message & { index: number })[]
|
||||
topic?: Topic
|
||||
topic: Topic
|
||||
hidePresetMessages?: boolean
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onDeleteGroupMessages?: (askId: string) => Promise<void>
|
||||
onGetMessages: () => Message[]
|
||||
onSetMessages: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage: (message: Message) => Promise<void>
|
||||
onDeleteGroupMessages: (askId: string) => Promise<void>
|
||||
}
|
||||
|
||||
const MessageGroup: FC<Props> = ({
|
||||
const MessageGroup = ({
|
||||
messages,
|
||||
topic,
|
||||
hidePresetMessages,
|
||||
@ -29,7 +30,7 @@ const MessageGroup: FC<Props> = ({
|
||||
onSetMessages,
|
||||
onGetMessages,
|
||||
onDeleteGroupMessages
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -43,7 +44,10 @@ const MessageGroup: FC<Props> = ({
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
const isGrid = multiModelMessageStyle === 'grid'
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
const handleDeleteGroup = useCallback(async () => {
|
||||
const askId = messages[0]?.askId
|
||||
if (!askId) return
|
||||
|
||||
window.modal.confirm({
|
||||
title: t('message.group.delete.title'),
|
||||
content: t('message.group.delete.content'),
|
||||
@ -52,10 +56,7 @@ const MessageGroup: FC<Props> = ({
|
||||
danger: true
|
||||
},
|
||||
okText: t('common.delete'),
|
||||
onOk: () => {
|
||||
const askId = messages[0].askId
|
||||
askId && onDeleteGroupMessages?.(askId)
|
||||
}
|
||||
onOk: () => onDeleteGroupMessages(askId)
|
||||
})
|
||||
}, [messages, onDeleteGroupMessages, t])
|
||||
|
||||
@ -63,6 +64,72 @@ const MessageGroup: FC<Props> = ({
|
||||
setSelectedIndex(messageLength - 1)
|
||||
}, [messageLength])
|
||||
|
||||
const renderMessage = useCallback(
|
||||
(message: Message & { index: number }, index: number) => {
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
const messageProps = {
|
||||
isGrouped,
|
||||
message,
|
||||
topic,
|
||||
index: message.index,
|
||||
hidePresetMessages,
|
||||
style: {
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
|
||||
},
|
||||
onSetMessages,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
}
|
||||
|
||||
const messageWrapper = (
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||
<MessageStream {...messageProps} />
|
||||
</MessageWrapper>
|
||||
)
|
||||
|
||||
if (isGridGroupMessage) {
|
||||
return (
|
||||
<Popover
|
||||
key={message.id}
|
||||
content={
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
$isInPopover={true}>
|
||||
<MessageStream {...messageProps} />
|
||||
</MessageWrapper>
|
||||
}
|
||||
trigger={gridPopoverTrigger}
|
||||
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}>
|
||||
{messageWrapper}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
return messageWrapper
|
||||
},
|
||||
[
|
||||
isGrid,
|
||||
isGrouped,
|
||||
isHorizontal,
|
||||
multiModelMessageStyle,
|
||||
selectedIndex,
|
||||
topic,
|
||||
hidePresetMessages,
|
||||
onSetMessages,
|
||||
onDeleteMessage,
|
||||
onGetMessages,
|
||||
gridPopoverTrigger
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<GroupContainer
|
||||
$isGrouped={isGrouped}
|
||||
@ -73,86 +140,7 @@ const MessageGroup: FC<Props> = ({
|
||||
$layout={multiModelMessageStyle}
|
||||
$gridColumns={gridColumns}
|
||||
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||
{messages.map((message, index) => {
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
if (isGridGroupMessage) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
$isInPopover={true}
|
||||
key={message.id}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={{
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
|
||||
}}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
}
|
||||
trigger={gridPopoverTrigger}
|
||||
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
|
||||
key={message.id}>
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={
|
||||
gridPopoverTrigger === 'hover' && isGrouped
|
||||
? {
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
|
||||
overflow: isGrouped ? 'hidden' : 'auto',
|
||||
maxHeight: isGrouped ? '280px' : 'unset'
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={{ paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15 }}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
)
|
||||
})}
|
||||
{messages.map((message, index) => renderMessage(message, index))}
|
||||
</GridContainer>
|
||||
{isGrouped && (
|
||||
<MessageGroupMenuBar
|
||||
@ -161,7 +149,7 @@ const MessageGroup: FC<Props> = ({
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
onDelete={onDelete}
|
||||
onDelete={handleDeleteGroup}
|
||||
/>
|
||||
)}
|
||||
</GroupContainer>
|
||||
|
||||
@ -19,13 +19,18 @@ 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 { Message, Model } from '@renderer/types'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
captureScrollableDivAsBlob,
|
||||
captureScrollableDivAsDataURL,
|
||||
removeTrailingDoubleSpaces,
|
||||
uuid
|
||||
} from '@renderer/utils'
|
||||
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'
|
||||
import {
|
||||
exportMarkdownToNotion,
|
||||
exportMarkdownToYuque,
|
||||
@ -35,21 +40,21 @@ import {
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistantModel?: Model
|
||||
model?: Model
|
||||
assistant: Assistant
|
||||
topic: Topic
|
||||
model: Model
|
||||
index?: number
|
||||
isGrouped?: boolean
|
||||
isLastMessage: boolean
|
||||
isAssistantMessage: boolean
|
||||
messageContainerRef: React.RefObject<HTMLDivElement>
|
||||
setModel: (model: Model) => void
|
||||
onEditMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
@ -59,18 +64,21 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
message,
|
||||
index,
|
||||
isGrouped,
|
||||
model,
|
||||
isLastMessage,
|
||||
isAssistantMessage,
|
||||
assistantModel,
|
||||
assistant,
|
||||
topic,
|
||||
model,
|
||||
messageContainerRef,
|
||||
onEditMessage,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
} = 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 isUserMessage = message.role === 'user'
|
||||
|
||||
@ -88,50 +96,33 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onNewBranch = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||
window.message.success({
|
||||
content: t('chat.message.new.branch.created'),
|
||||
key: 'new-branch'
|
||||
})
|
||||
window.message.success({ content: t('chat.message.new.branch.created'), key: 'new-branch' })
|
||||
}, [index, t])
|
||||
|
||||
const onResend = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const _messages = onGetMessages?.() || []
|
||||
const groupdMessages = _messages.filter((m) => m.askId === message.id)
|
||||
const handleResendUserMessage = useCallback(
|
||||
async (messageUpdate?: Message) => {
|
||||
// messageUpdate 为了处理用户消息更改后的message
|
||||
await modelGenerating()
|
||||
const groupdMessages = messages.filter((m) => m.askId === message.id)
|
||||
|
||||
// Resend all groupd messages
|
||||
if (!isEmpty(groupdMessages)) {
|
||||
for (const assistantMessage of groupdMessages) {
|
||||
const _model = assistantMessage.model || assistantModel
|
||||
EventEmitter.emit(
|
||||
EVENT_NAMES.RESEND_MESSAGE + ':' + assistantMessage.id,
|
||||
resetAssistantMessage(assistantMessage, _model)
|
||||
)
|
||||
// Resend all grouped messages
|
||||
if (!isEmpty(groupdMessages)) {
|
||||
for (const assistantMessage of groupdMessages) {
|
||||
const _model = assistantMessage.model || assistantModel
|
||||
await dispatch(resendMessage({ ...assistantMessage, model: _model }, assistant, topic))
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If there is no groupd message, resend next message
|
||||
const index = _messages.findIndex((m) => m.id === message.id)
|
||||
const nextIndex = index + 1
|
||||
const nextMessage = _messages[nextIndex]
|
||||
await dispatch(resendMessage(messageUpdate ?? message, assistant, topic))
|
||||
},
|
||||
[message, assistantModel, model, onDeleteMessage, onGetMessages, dispatch, assistant, topic]
|
||||
)
|
||||
|
||||
if (nextMessage && nextMessage.role === 'assistant') {
|
||||
EventEmitter.emit(EVENT_NAMES.RESEND_MESSAGE + ':' + nextMessage.id, {
|
||||
...nextMessage,
|
||||
content: '',
|
||||
status: 'sending',
|
||||
model: assistantModel || model,
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// If next message is not exist or next message role is user, delete current message and resend
|
||||
if (!nextMessage || nextMessage.role === 'user') {
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
|
||||
onDeleteMessage?.(message)
|
||||
}
|
||||
}, [assistantModel, message, model, onDeleteMessage, onGetMessages])
|
||||
// const onResendUserMessage = useCallback(async () => {
|
||||
// // await dispatch(resendMessage(message, assistant, topic))
|
||||
// onResend()
|
||||
// }, [message, dispatch, assistant, topic])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
let resendMessage = false
|
||||
@ -152,43 +143,54 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
) : null
|
||||
}
|
||||
})
|
||||
if (editedText && editedText !== message.content) {
|
||||
// 同步修改store中用户消息
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { content: editedText } }))
|
||||
|
||||
if (editedText) {
|
||||
await onEditMessage?.({ ...message, content: editedText })
|
||||
// const updatedMessages = onGetMessages?.() || []
|
||||
// dispatch(updateMessages(topic, updatedMessages))
|
||||
}
|
||||
|
||||
resendMessage && onResend()
|
||||
}, [message, onEditMessage, onResend, t])
|
||||
|
||||
const onResendUserMessage = useCallback(async () => {
|
||||
await onEditMessage?.({ ...message, content: message.content })
|
||||
onResend && onResend()
|
||||
}, [message, onEditMessage, onResend])
|
||||
if (resendMessage) handleResendUserMessage({ ...message, content: editedText })
|
||||
}, [message, dispatch, topic, onGetMessages, handleResendUserMessage, t])
|
||||
|
||||
const handleTranslate = useCallback(
|
||||
async (language: string) => {
|
||||
if (isTranslating) return
|
||||
|
||||
onEditMessage?.({ ...message, translatedContent: t('translate.processing') })
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: message.id,
|
||||
updates: { translatedContent: t('translate.processing') }
|
||||
})
|
||||
)
|
||||
|
||||
setIsTranslating(true)
|
||||
|
||||
try {
|
||||
await translateText(message.content, language, (text) =>
|
||||
onEditMessage?.({ ...message, translatedContent: text })
|
||||
)
|
||||
await translateText(message.content, language, (text) => {
|
||||
// 使用 setStreamMessage 来更新翻译内容
|
||||
dispatch(
|
||||
setStreamMessage({
|
||||
topicId: topic.id,
|
||||
message: { ...message, translatedContent: text }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// 翻译完成后,提交流消息
|
||||
dispatch(commitStreamMessage({ topicId: topic.id }))
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
window.message.error({
|
||||
content: t('translate.error.failed'),
|
||||
key: 'translate-message'
|
||||
})
|
||||
onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
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 }))
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
},
|
||||
[isTranslating, message, onEditMessage, t]
|
||||
[isTranslating, message, dispatch, topic, t]
|
||||
)
|
||||
|
||||
const dropdownItems = useMemo(
|
||||
@ -202,18 +204,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
window.api.file.save(fileName, message.content)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
onClick: onEdit
|
||||
},
|
||||
{
|
||||
label: t('chat.message.new.branch'),
|
||||
key: 'new-branch',
|
||||
icon: <ForkOutlined />,
|
||||
onClick: onNewBranch
|
||||
},
|
||||
{ label: t('common.edit'), key: 'edit', icon: <EditOutlined />, onClick: onEdit },
|
||||
{ label: t('chat.message.new.branch'), key: 'new-branch', icon: <ForkOutlined />, onClick: onNewBranch },
|
||||
{
|
||||
label: t('chat.topics.export.title'),
|
||||
key: 'export',
|
||||
@ -241,11 +233,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.md'),
|
||||
key: 'markdown',
|
||||
onClick: () => exportMessageAsMarkdown(message)
|
||||
},
|
||||
{ label: t('chat.topics.export.md'), key: 'markdown', onClick: () => exportMessageAsMarkdown(message) },
|
||||
|
||||
{
|
||||
label: t('chat.topics.export.word'),
|
||||
@ -284,7 +272,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
await modelGenerating()
|
||||
const selectedModel = isGrouped ? model : assistantModel
|
||||
const _message = resetAssistantMessage(message, selectedModel)
|
||||
onEditMessage?.(_message)
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: _message }))
|
||||
dispatch(resendMessage(_message, assistant, topic))
|
||||
}
|
||||
|
||||
const onMentionModel = async (e: React.MouseEvent) => {
|
||||
@ -293,28 +282,24 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
if (!selectedModel) return
|
||||
|
||||
const _message: Message = resetAssistantMessage(message, selectedModel)
|
||||
|
||||
if (message.askId && message.model) {
|
||||
return EventEmitter.emit(EVENT_NAMES.APPEND_MESSAGE, { ..._message, id: uuid() })
|
||||
}
|
||||
|
||||
onEditMessage?.(_message)
|
||||
// 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))
|
||||
}
|
||||
|
||||
const onUseful = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onEditMessage?.({ ...message, useful: !message.useful })
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { useful: !message.useful } }))
|
||||
},
|
||||
[message, onEditMessage]
|
||||
[message, dispatch, topic]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onResendUserMessage}>
|
||||
<ActionButton className="message-action-button" onClick={() => handleResendUserMessage()}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@ -365,7 +350,14 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
onClick: () =>
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: message.id,
|
||||
updates: { translatedContent: undefined }
|
||||
})
|
||||
)
|
||||
}
|
||||
],
|
||||
onClick: (e) => e.domEvent.stopPropagation()
|
||||
@ -467,4 +459,4 @@ const ReSendButton = styled(Button)`
|
||||
left: 0;
|
||||
`
|
||||
|
||||
export default MessageMenubar
|
||||
export default memo(MessageMenubar)
|
||||
|
||||
79
src/renderer/src/pages/home/Messages/MessageStream.tsx
Normal file
79
src/renderer/src/pages/home/Messages/MessageStream.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { selectStreamMessage } from '@renderer/store/messages'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
|
||||
interface MessageStreamProps {
|
||||
message: Message
|
||||
topic: Topic
|
||||
assistant?: Assistant
|
||||
index?: number
|
||||
hidePresetMessages?: boolean
|
||||
isGrouped?: boolean
|
||||
style?: React.CSSProperties
|
||||
onSetMessages?: React.Dispatch<React.SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
|
||||
const MessageStreamContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`
|
||||
|
||||
const MessageStream: React.FC<MessageStreamProps> = ({
|
||||
message: _message,
|
||||
topic,
|
||||
assistant,
|
||||
index,
|
||||
hidePresetMessages,
|
||||
isGrouped,
|
||||
style,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages
|
||||
}) => {
|
||||
// 获取流式消息
|
||||
const streamMessage = useAppSelector((state) => selectStreamMessage(state, _message.topicId, _message.id))
|
||||
// 获取常规消息
|
||||
const regularMessage = useAppSelector((state) => {
|
||||
// 如果是用户消息,直接使用传入的_message
|
||||
if (_message.role === 'user') {
|
||||
return _message
|
||||
}
|
||||
|
||||
// 对于助手消息,从store中查找最新状态
|
||||
const topicMessages = state.messages.messagesByTopic[_message.topicId]
|
||||
if (!topicMessages) return _message
|
||||
|
||||
return topicMessages.find((m) => m.id === _message.id) || _message
|
||||
})
|
||||
|
||||
// 在hooks调用后进行条件判断
|
||||
const isStreaming = !!(streamMessage && streamMessage.id === _message.id)
|
||||
const message = isStreaming ? streamMessage : regularMessage
|
||||
// console.log('streamMessage', streamMessage)
|
||||
// console.log('regularMessage', regularMessage)
|
||||
return (
|
||||
<MessageStreamContainer>
|
||||
<MessageItem
|
||||
message={message}
|
||||
topic={topic}
|
||||
assistant={assistant}
|
||||
index={index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
isGrouped={isGrouped}
|
||||
style={style}
|
||||
isStreaming={isStreaming}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageStreamContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageStream
|
||||
@ -1,25 +1,27 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import db from '@renderer/databases'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { getTopic } from '@renderer/hooks/useTopic'
|
||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import {
|
||||
deleteMessageFiles,
|
||||
getAssistantMessage,
|
||||
getContextCount,
|
||||
getGroupedMessages,
|
||||
getUserMessage
|
||||
} from '@renderer/services/MessagesService'
|
||||
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
|
||||
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
clearTopicMessages,
|
||||
selectDisplayCount,
|
||||
selectLoading,
|
||||
selectTopicMessages,
|
||||
updateMessages
|
||||
} from '@renderer/store/messages'
|
||||
import type { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, runAsyncFunction } from '@renderer/utils'
|
||||
import { t } from 'i18next'
|
||||
import { flatten, last, take } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { last } from 'lodash'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
@ -29,29 +31,48 @@ import MessageGroup from './MessageGroup'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import Prompt from './Prompt'
|
||||
|
||||
interface Props {
|
||||
interface MessagesProps {
|
||||
assistant: Assistant
|
||||
topic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
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(true)
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const messagesRef = useRef(messages)
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
|
||||
useEffect(() => {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
const newDisplayMessages = reversedMessages.slice(0, displayCount)
|
||||
|
||||
const groupedMessages = getGroupedMessages(displayMessages)
|
||||
setDisplayMessages(newDisplayMessages)
|
||||
setHasMore(messages.length > displayCount)
|
||||
}, [messages, displayCount])
|
||||
|
||||
const INITIAL_MESSAGES_COUNT = 20
|
||||
const LOAD_MORE_COUNT = 20
|
||||
const handleDeleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const newMessages = messages.filter((m) => m.id !== message.id)
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
},
|
||||
[dispatch, topic, messages]
|
||||
)
|
||||
|
||||
messagesRef.current = 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'
|
||||
@ -64,58 +85,27 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 50)
|
||||
}, [])
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const assistantMessages: Message[] = []
|
||||
// const onAppendMessageMemo = useCallback(
|
||||
// async (message: Message) => {
|
||||
// const newMessages = [...messages, message]
|
||||
// await dispatch(updateMessages(topic, newMessages))
|
||||
// },
|
||||
// [topic, dispatch, messages]
|
||||
// )
|
||||
|
||||
if (message.mentions?.length) {
|
||||
message.mentions.forEach((m) => {
|
||||
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
|
||||
assistantMessage.model = m
|
||||
assistantMessage.askId = message.id
|
||||
assistantMessages.push(assistantMessage)
|
||||
})
|
||||
} else {
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic })
|
||||
assistantMessage.askId = message.id
|
||||
assistantMessages.push(assistantMessage)
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
const messages = prev.concat([message, ...assistantMessages])
|
||||
db.topics.put({ id: topic.id, messages })
|
||||
return messages
|
||||
})
|
||||
|
||||
scrollToBottom()
|
||||
},
|
||||
[assistant, scrollToBottom, topic]
|
||||
)
|
||||
|
||||
const onAppendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
setMessages((prev) => {
|
||||
const messages = prev.concat([message])
|
||||
db.topics.put({ id: topic.id, messages })
|
||||
return messages
|
||||
})
|
||||
},
|
||||
[topic.id]
|
||||
)
|
||||
|
||||
const autoRenameTopic = useCallback(async () => {
|
||||
const autoRenameTopicMemo = useCallback(async () => {
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
|
||||
// If the topic auto naming is not enabled, use the first message content as the topic name
|
||||
if (!enableTopicNaming) {
|
||||
const topicName = messages[0].content.substring(0, 50)
|
||||
const data = { ..._topic, name: topicName } as Topic
|
||||
setActiveTopic(data)
|
||||
updateTopic(data)
|
||||
const topicName = messages[0]?.content.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ..._topic, name: topicName } as Topic
|
||||
setActiveTopic(data)
|
||||
updateTopic(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Auto rename the topic
|
||||
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
@ -124,60 +114,33 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
updateTopic(data)
|
||||
}
|
||||
}
|
||||
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic])
|
||||
|
||||
const onDeleteMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
await db.topics.update(topic.id, { messages: _messages })
|
||||
await deleteMessageFiles(message)
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
|
||||
const onDeleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const _messages = messages.filter((m) => m.askId !== askId && m.id !== askId)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
await db.topics.update(topic.id, { messages: _messages })
|
||||
for (const message of _messages) {
|
||||
await deleteMessageFiles(message)
|
||||
}
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
|
||||
const onGetMessages = useCallback(() => {
|
||||
return messagesRef.current
|
||||
}, [])
|
||||
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic, t])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage),
|
||||
EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessage),
|
||||
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async () => {
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||
// EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessageMemo),
|
||||
// EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, () => {
|
||||
// setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||
// }),
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, () => {
|
||||
scrollToBottom()
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, (data: Topic) => {
|
||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopicMemo),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, async (data: Topic) => {
|
||||
const defaultTopic = getDefaultTopic(assistant.id)
|
||||
|
||||
// Clear messages of other topics
|
||||
if (data && data.id !== topic.id) {
|
||||
TopicManager.clearTopicMessages(data.id)
|
||||
updateTopic({ ...data, name: defaultTopic.name, messages: [] })
|
||||
await dispatch(clearTopicMessages(data.id))
|
||||
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear messages of current topic
|
||||
setMessages([])
|
||||
await dispatch(clearTopicMessages(topic.id))
|
||||
setDisplayMessages([])
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
_topic && updateTopic({ ..._topic, name: defaultTopic.name, messages: [] })
|
||||
TopicManager.clearTopicMessages(topic.id)
|
||||
if (_topic) {
|
||||
updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
|
||||
}
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
||||
@ -192,68 +155,29 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
window.api.file.saveImage(topic.name, imageData)
|
||||
}
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => {
|
||||
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, async () => {
|
||||
const lastMessage = last(messages)
|
||||
|
||||
if (lastMessage && lastMessage.type === 'clear') {
|
||||
onDeleteMessage(lastMessage)
|
||||
if (lastMessage?.type === 'clear') {
|
||||
handleDeleteMessage(lastMessage)
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
const messages = prev.concat([getUserMessage({ assistant, topic, type: 'clear' })])
|
||||
db.topics.put({ id: topic.id, messages })
|
||||
return messages
|
||||
})
|
||||
if (messages.length === 0) return
|
||||
|
||||
const clearMessage = getUserMessage({ assistant, topic, type: 'clear' })
|
||||
const newMessages = [...messages, clearMessage]
|
||||
await dispatch(updateMessages(topic, newMessages))
|
||||
scrollToBottom()
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
|
||||
const newTopic = getDefaultTopic(assistant.id)
|
||||
newTopic.name = topic.name
|
||||
const branchMessages = take(messages, messages.length - index)
|
||||
|
||||
// 将分支的消息放入数据库
|
||||
await db.topics.add({ id: newTopic.id, messages: branchMessages })
|
||||
addTopic(newTopic)
|
||||
setActiveTopic(newTopic)
|
||||
autoRenameTopic()
|
||||
|
||||
// 由于复制了消息,消息中附带的文件的总数变了,需要更新
|
||||
const filesArr = branchMessages.map((m) => m.files)
|
||||
const files = flatten(filesArr).filter(Boolean)
|
||||
files.map(async (f) => {
|
||||
const file = await db.files.get({ id: f?.id })
|
||||
file && db.files.update(file.id, { count: file.count + 1 })
|
||||
})
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [
|
||||
addTopic,
|
||||
assistant,
|
||||
autoRenameTopic,
|
||||
messages,
|
||||
onAppendMessage,
|
||||
onDeleteMessage,
|
||||
onSendMessage,
|
||||
scrollToBottom,
|
||||
setActiveTopic,
|
||||
topic,
|
||||
updateTopic
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const messages = (await TopicManager.getTopicMessages(topic.id)) || []
|
||||
setMessages(messages)
|
||||
})
|
||||
}, [topic.id])
|
||||
return () => {
|
||||
for (const unsub of unsubscribes) {
|
||||
unsub()
|
||||
}
|
||||
}
|
||||
}, [assistant, autoRenameTopicMemo, dispatch, messages, handleDeleteMessage, scrollToBottom, topic, updateTopic])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@ -264,21 +188,10 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
})
|
||||
}, [assistant, messages])
|
||||
|
||||
// 初始化显示最新的消息
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
setDisplayMessages(reversedMessages.slice(0, INITIAL_MESSAGES_COUNT))
|
||||
setHasMore(messages.length > INITIAL_MESSAGES_COUNT)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// 加载更多历史消息
|
||||
const loadMoreMessages = useCallback(() => {
|
||||
if (!hasMore || isLoadingMore) return
|
||||
|
||||
setIsLoadingMore(true)
|
||||
|
||||
setTimeout(() => {
|
||||
const currentLength = displayMessages.length
|
||||
const reversedMessages = [...messages].reverse()
|
||||
@ -288,7 +201,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 300)
|
||||
}, [displayMessages, hasMore, isLoadingMore, messages])
|
||||
}, [displayMessages.length, hasMore, isLoadingMore, messages])
|
||||
|
||||
useShortcut('copy_last_message', () => {
|
||||
const lastMessage = last(messages)
|
||||
@ -315,19 +228,19 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<LoaderContainer $loading={loading || isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{Object.entries(groupedMessages).map(([key, messages]) => (
|
||||
{Object.entries(getGroupedMessages(displayMessages)).map(([key, groupMessages]) => (
|
||||
<MessageGroup
|
||||
key={key}
|
||||
messages={messages}
|
||||
messages={groupMessages}
|
||||
topic={topic}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onDeleteGroupMessages={onDeleteGroupMessages}
|
||||
onGetMessages={onGetMessages}
|
||||
onSetMessages={setDisplayMessages}
|
||||
onDeleteMessage={handleDeleteMessage}
|
||||
onDeleteGroupMessages={handleDeleteGroupMessages}
|
||||
onGetMessages={() => messages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { fetchSuggestions } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { sendMessage } from '@renderer/store/messages'
|
||||
import { Assistant, Message, Suggestion } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { last } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, memo, useEffect, useState } from 'react'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
messages: Message[]
|
||||
@ -16,40 +14,46 @@ interface Props {
|
||||
const suggestionsMap = new Map<string, Suggestion[]>()
|
||||
|
||||
const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>(
|
||||
suggestionsMap.get(messages[messages.length - 1]?.id) || []
|
||||
)
|
||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
|
||||
|
||||
const onClick = (s: Suggestion) => {
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: s.content,
|
||||
assistantId: assistant.id,
|
||||
topicId: assistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
type: 'text',
|
||||
status: 'success'
|
||||
}
|
||||
const handleSuggestionClick = async (content: string) => {
|
||||
await dispatch(sendMessage(content, assistant, assistant.topics[0]))
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
const suggestionsHandle = async () => {
|
||||
if (loadingSuggestions) return
|
||||
try {
|
||||
setLoadingSuggestions(true)
|
||||
const _suggestions = await fetchSuggestions({
|
||||
assistant,
|
||||
messages
|
||||
})
|
||||
if (_suggestions.length) {
|
||||
setSuggestions(_suggestions)
|
||||
suggestionsMap.set(messages[messages.length - 1].id, _suggestions)
|
||||
}
|
||||
} finally {
|
||||
setLoadingSuggestions(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
||||
setLoadingSuggestions(true)
|
||||
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
|
||||
if (_suggestions.length) {
|
||||
setSuggestions(_suggestions)
|
||||
suggestionsMap.set(msg.id, _suggestions)
|
||||
}
|
||||
setLoadingSuggestions(false)
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant, messages])
|
||||
suggestionsHandle()
|
||||
// const unsubscribes = [
|
||||
// EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
||||
|
||||
// ]
|
||||
// return () => {
|
||||
// for (const unsub of unsubscribes) {
|
||||
// unsub()
|
||||
// }
|
||||
// }
|
||||
}, []) // Remove messages dependency
|
||||
|
||||
useEffect(() => {
|
||||
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
|
||||
@ -58,7 +62,6 @@ const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
||||
if (last(messages)?.status !== 'success') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (loadingSuggestions) {
|
||||
return (
|
||||
<Container>
|
||||
@ -75,7 +78,7 @@ const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
||||
<Container>
|
||||
<SuggestionsContainer>
|
||||
{suggestions.map((s, i) => (
|
||||
<SuggestionItem key={i} onClick={() => onClick(s)}>
|
||||
<SuggestionItem key={i} onClick={() => handleSuggestionClick(s.content)}>
|
||||
{s.content} →
|
||||
</SuggestionItem>
|
||||
))}
|
||||
@ -117,4 +120,4 @@ const SuggestionItem = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default Suggestions
|
||||
export default memo(Suggestions)
|
||||
|
||||
@ -129,6 +129,7 @@ export async function fetchChatCompletion({
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('error', error)
|
||||
message.status = 'error'
|
||||
message.error = formatMessageError(error)
|
||||
}
|
||||
@ -216,7 +217,6 @@ export async function fetchSuggestions({
|
||||
assistant: Assistant
|
||||
}): Promise<Suggestion[]> {
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
return []
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ export const EventEmitter = new Emittery()
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
APPEND_MESSAGE: 'APPEND_MESSAGE',
|
||||
// APPEND_MESSAGE: 'APPEND_MESSAGE',
|
||||
RECEIVE_MESSAGE: 'RECEIVE_MESSAGE',
|
||||
AI_AUTO_RENAME: 'AI_AUTO_RENAME',
|
||||
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
||||
|
||||
@ -8,6 +8,7 @@ import assistants from './assistants'
|
||||
import knowledge from './knowledge'
|
||||
import llm from './llm'
|
||||
import mcp from './mcp'
|
||||
import messagesReducer from './messages'
|
||||
import migrate from './migrate'
|
||||
import minapps from './minapps'
|
||||
import paintings from './paintings'
|
||||
@ -27,6 +28,7 @@ const rootReducer = combineReducers({
|
||||
knowledge,
|
||||
minapps,
|
||||
websearch,
|
||||
messages: messagesReducer,
|
||||
mcp
|
||||
})
|
||||
|
||||
@ -35,7 +37,7 @@ const persistedReducer = persistReducer(
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 77,
|
||||
blacklist: ['runtime'],
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
rootReducer
|
||||
|
||||
509
src/renderer/src/store/messages.ts
Normal file
509
src/renderer/src/store/messages.ts
Normal file
@ -0,0 +1,509 @@
|
||||
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSelector } 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 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
|
||||
displayCount: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: MessagesState = {
|
||||
messagesByTopic: {},
|
||||
streamMessagesByTopic: {},
|
||||
currentTopic: '',
|
||||
loading: false,
|
||||
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[]> = {}
|
||||
|
||||
// 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 }))
|
||||
}
|
||||
}
|
||||
|
||||
return messagesByTopic
|
||||
})
|
||||
|
||||
const messagesSlice = createSlice({
|
||||
name: 'messages',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload
|
||||
},
|
||||
setDisplayCount: (state, action: PayloadAction<number>) => {
|
||||
state.displayCount = action.payload
|
||||
},
|
||||
addMessage: (state, action: PayloadAction<{ topicId: string; messages: Message | Message[] }>) => {
|
||||
const { topicId, messages } = action.payload
|
||||
if (!state.messagesByTopic[topicId]) {
|
||||
state.messagesByTopic[topicId] = []
|
||||
}
|
||||
if (Array.isArray(messages)) {
|
||||
// 为了兼容多模型新发消息,一次性添加多个助手消息
|
||||
// 不是什么好主意,不符合语义
|
||||
state.messagesByTopic[topicId].push(...messages)
|
||||
} else {
|
||||
state.messagesByTopic[topicId].push(messages)
|
||||
}
|
||||
},
|
||||
updateMessage: (
|
||||
state,
|
||||
action: PayloadAction<{ topicId: string; messageId: string; updates: Partial<Message> }>
|
||||
) => {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
},
|
||||
setCurrentTopic: (state, action: PayloadAction<string>) => {
|
||||
state.currentTopic = action.payload
|
||||
},
|
||||
clearTopicMessages: (state, action: PayloadAction<string>) => {
|
||||
const topicId = action.payload
|
||||
state.messagesByTopic[topicId] = []
|
||||
state.error = null
|
||||
},
|
||||
loadTopicMessages: (state, action: PayloadAction<{ topicId: string; messages: Message[] }>) => {
|
||||
const { topicId, messages } = action.payload
|
||||
state.messagesByTopic[topicId] = messages.map((msg) => ({ ...msg }))
|
||||
},
|
||||
setStreamMessage: (state, action: PayloadAction<{ topicId: string; message: Message | null }>) => {
|
||||
const { topicId, message } = action.payload
|
||||
if (!state.streamMessagesByTopic[topicId]) {
|
||||
state.streamMessagesByTopic[topicId] = {}
|
||||
}
|
||||
if (message) {
|
||||
state.streamMessagesByTopic[topicId][message.id] = message
|
||||
}
|
||||
},
|
||||
commitStreamMessage: (state, action: PayloadAction<{ topicId: string; messageId: string }>) => {
|
||||
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)
|
||||
}
|
||||
|
||||
// 只删除这个特定消息的流状态
|
||||
delete state.streamMessagesByTopic[topicId][messageId]
|
||||
},
|
||||
clearStreamMessage: (state, action: PayloadAction<{ topicId: string; messageId: string }>) => {
|
||||
const { topicId, messageId } = action.payload
|
||||
if (state.streamMessagesByTopic[topicId]) {
|
||||
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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const {
|
||||
setLoading,
|
||||
setError,
|
||||
setDisplayCount,
|
||||
addMessage,
|
||||
updateMessage,
|
||||
setCurrentTopic,
|
||||
clearTopicMessages,
|
||||
loadTopicMessages,
|
||||
setStreamMessage,
|
||||
commitStreamMessage,
|
||||
clearStreamMessage
|
||||
} = messagesSlice.actions
|
||||
|
||||
const handleResponseMessageUpdate = (message, topicId, dispatch, getState) => {
|
||||
dispatch(setStreamMessage({ topicId, message }))
|
||||
// When message is complete, commit to messages and sync with DB
|
||||
if (message.status !== 'pending') {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
// Modified sendMessage thunk
|
||||
export const sendMessage =
|
||||
(
|
||||
content: string,
|
||||
assistant: Assistant,
|
||||
topic: Topic,
|
||||
options?: {
|
||||
files?: FileType[]
|
||||
knowledgeBaseIds?: string[]
|
||||
mentionModels?: Model[]
|
||||
resendUserMessage?: Message
|
||||
resendAssistantMessage?: Message
|
||||
enabledMCPs?: MCPServer[]
|
||||
}
|
||||
) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
|
||||
// Initialize topic messages if not exists
|
||||
const initialState = getState()
|
||||
if (!initialState.messages.messagesByTopic[topic.id]) {
|
||||
dispatch(clearTopicMessages(topic.id))
|
||||
}
|
||||
|
||||
// 判断是否重发消息
|
||||
const isResend = !!options?.resendUserMessage
|
||||
|
||||
// 使用用户消息
|
||||
let userMessage: Message
|
||||
if (isResend) {
|
||||
userMessage = options.resendUserMessage
|
||||
} else {
|
||||
// 创建新的用户消息
|
||||
userMessage = getUserMessage({ assistant, topic, type: 'text', content })
|
||||
|
||||
if (options?.files) {
|
||||
userMessage.files = options.files
|
||||
}
|
||||
if (options?.knowledgeBaseIds) {
|
||||
userMessage.knowledgeBaseIds = options.knowledgeBaseIds
|
||||
}
|
||||
if (options?.mentionModels) {
|
||||
userMessage.mentions = options.mentionModels
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是重发,才添加新的用户消息
|
||||
if (!isResend) {
|
||||
dispatch(addMessage({ topicId: topic.id, messages: userMessage }))
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
||||
|
||||
// 处理助手消息
|
||||
// let assistantMessage: Message
|
||||
let assistantMessages: Message[] = []
|
||||
|
||||
// 使用助手消息
|
||||
if (isResend && options.resendAssistantMessage) {
|
||||
// 直接使用传入的助手消息,进行重置
|
||||
const messageToReset = options.resendAssistantMessage
|
||||
const { model, id } = messageToReset
|
||||
const resetMessage = resetAssistantMessage(messageToReset, model)
|
||||
// 更新状态
|
||||
dispatch(updateMessage({ topicId: topic.id, messageId: id, updates: resetMessage }))
|
||||
|
||||
// 使用重置后的消息
|
||||
assistantMessages.push(resetMessage)
|
||||
} else {
|
||||
// 不是重发情况
|
||||
// 为每个被 mention 的模型创建一个助手消息
|
||||
if (options?.mentionModels?.length) {
|
||||
assistantMessages = options.mentionModels.map((m) => {
|
||||
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
|
||||
assistantMessage.model = m
|
||||
assistantMessage.askId = userMessage.id
|
||||
assistantMessage.status = 'sending'
|
||||
return assistantMessage
|
||||
})
|
||||
} else {
|
||||
// 创建新的助手消息
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic })
|
||||
assistantMessage.askId = userMessage.id
|
||||
assistantMessage.status = 'sending'
|
||||
assistantMessages.push(assistantMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Use topic queue to handle request
|
||||
const queue = getTopicQueue(topic.id)
|
||||
// let assistantMessage: Message | undefined
|
||||
!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 }))
|
||||
|
||||
// Sync user message with database
|
||||
const state = getState()
|
||||
const currentTopicMessages = state.messages.messagesByTopic[topic.id]
|
||||
if (currentTopicMessages) {
|
||||
await syncMessagesWithDB(topic.id, currentTopicMessages)
|
||||
}
|
||||
queue.add(async () => {
|
||||
try {
|
||||
const state = getState()
|
||||
const topicMessages = state.messages.messagesByTopic[topic.id]
|
||||
if (!topicMessages) {
|
||||
dispatch(clearTopicMessages(topic.id))
|
||||
return
|
||||
}
|
||||
|
||||
const messages = convertToDBFormat(topicMessages)
|
||||
|
||||
// Prepare assistant config
|
||||
const assistantWithModel = assistantMessage.model
|
||||
? { ...assistant, model: assistantMessage.model }
|
||||
: assistant
|
||||
|
||||
if (topic.prompt) {
|
||||
assistantWithModel.prompt = assistantWithModel.prompt
|
||||
? `${assistantWithModel.prompt}\n${topic.prompt}`
|
||||
: topic.prompt
|
||||
}
|
||||
|
||||
// 节流
|
||||
const throttledDispatch = throttle(handleResponseMessageUpdate, 100, { trailing: true }) // 100ms的节流时间应足够平衡用户体验和性能
|
||||
|
||||
await fetchChatCompletion({
|
||||
message: { ...assistantMessage },
|
||||
messages: messages
|
||||
.filter((m) => !m.status?.includes('ing'))
|
||||
.slice(
|
||||
0,
|
||||
messages.findIndex((m) => m.id === assistantMessage.id)
|
||||
),
|
||||
assistant: assistantWithModel,
|
||||
onResponse: async (msg) => {
|
||||
// 允许在回调外维护一个最新的消息状态,每次都更新这个对象,但只通过节流函数分发到Redux
|
||||
const updatedMsg = { ...msg, status: msg.status || 'pending', content: msg.content || '' }
|
||||
// 创建节流函数,限制Redux更新频率
|
||||
// 使用节流函数更新Redux
|
||||
throttledDispatch({ ...assistantMessage, ...updatedMsg }, topic.id, dispatch, getState)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in chat completion:', error)
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: topic.id,
|
||||
messageId: assistantMessage.id,
|
||||
updates: { status: 'error', error: { message: error.message } }
|
||||
})
|
||||
)
|
||||
dispatch(clearStreamMessage({ topicId: topic.id, messageId: assistantMessage.id }))
|
||||
dispatch(setError(error.message))
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in sendMessage:', error)
|
||||
dispatch(setError(error.message))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
// resendMessage thunk,专门用于重发消息和在助手消息下@新模型
|
||||
// 本质都是重发助手消息,兼容了两种消息类型,以及@新模型(属于追加助手消息之后重发)
|
||||
export const resendMessage =
|
||||
(message: Message, assistant: Assistant, topic: Topic, isMentionModel = false) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
try {
|
||||
// 获取状态
|
||||
const state = getState()
|
||||
const topicMessages = state.messages.messagesByTopic[topic.id] || []
|
||||
|
||||
// 如果是用户消息,直接重发
|
||||
if (message.role === 'user') {
|
||||
// 查找此用户消息对应的助手消息
|
||||
const assistantMessage = topicMessages.find((m) => m.role === 'assistant' && m.askId === message.id)
|
||||
|
||||
dispatch(
|
||||
sendMessage(message.content, assistant, topic, {
|
||||
resendUserMessage: message,
|
||||
resendAssistantMessage: assistantMessage
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 如果是助手消息,找到对应的用户消息
|
||||
const userMessage = topicMessages.find((m) => m.id === message.askId && m.role === 'user')
|
||||
|
||||
if (!userMessage) {
|
||||
console.error('Cannot find original user message to resend')
|
||||
dispatch(setError('Cannot find original user message to resend'))
|
||||
return
|
||||
}
|
||||
|
||||
if (isMentionModel) {
|
||||
// @
|
||||
return dispatch(
|
||||
sendMessage(userMessage.content, assistant, topic, {
|
||||
resendUserMessage: userMessage
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
dispatch(
|
||||
sendMessage(userMessage.content, assistant, topic, {
|
||||
resendUserMessage: userMessage,
|
||||
resendAssistantMessage: message
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error in resendMessage:', error)
|
||||
dispatch(setError(error.message))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified loadTopicMessages thunk
|
||||
export const loadTopicMessagesThunk = (topicId: string) => 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))
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified clearMessages thunk
|
||||
export const clearTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
|
||||
// Wait for any pending requests to complete
|
||||
await waitForTopicQueue(topic.id)
|
||||
|
||||
// Clear the topic's request queue
|
||||
clearTopicQueue(topic.id)
|
||||
|
||||
// Clear messages from state and database
|
||||
dispatch(clearTopicMessages(topic.id))
|
||||
await db.topics.update(topic.id, { messages: [] })
|
||||
|
||||
// Update current topic
|
||||
dispatch(setCurrentTopic(topic.id))
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to clear messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(false))
|
||||
}
|
||||
}
|
||||
|
||||
// Modified updateMessages thunk
|
||||
export const updateMessages = (topic: Topic, messages: Message[]) => async (dispatch: AppDispatch) => {
|
||||
try {
|
||||
dispatch(setLoading(true))
|
||||
await db.topics.update(topic.id, { messages })
|
||||
dispatch(loadTopicMessages({ topicId: topic.id, messages }))
|
||||
} catch (error) {
|
||||
dispatch(setError(error instanceof Error ? error.message : 'Failed to update messages'))
|
||||
} finally {
|
||||
dispatch(setLoading(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 selectLoading = (state: RootState): boolean => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
return messagesState?.loading || false
|
||||
}
|
||||
|
||||
export const selectDisplayCount = (state: RootState): number => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
return messagesState?.displayCount || 20
|
||||
}
|
||||
|
||||
export const selectError = (state: RootState): string | null => {
|
||||
const messagesState = state.messages as MessagesState
|
||||
return messagesState?.error || null
|
||||
}
|
||||
|
||||
export const selectStreamMessage = (state: RootState, topicId: string, messageId: string): Message | null =>
|
||||
state.messages.streamMessagesByTopic[topicId]?.[messageId] || null
|
||||
|
||||
export default messagesSlice.reducer
|
||||
72
src/renderer/src/utils/queue.ts
Normal file
72
src/renderer/src/utils/queue.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
// Queue configuration - managed by topic
|
||||
const requestQueues: { [topicId: string]: PQueue } = {}
|
||||
|
||||
/**
|
||||
* Get or create a queue for a specific topic
|
||||
* @param topicId The ID of the topic
|
||||
* @returns A PQueue instance for the topic
|
||||
*/
|
||||
export const getTopicQueue = (topicId: string): PQueue => {
|
||||
if (!requestQueues[topicId]) {
|
||||
requestQueues[topicId] = new PQueue({
|
||||
concurrency: 4,
|
||||
timeout: 1000 * 60 * 5, // 5 minutes
|
||||
throwOnTimeout: false
|
||||
})
|
||||
}
|
||||
return requestQueues[topicId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the queue for a specific topic
|
||||
* @param topicId The ID of the topic
|
||||
*/
|
||||
export const clearTopicQueue = (topicId: string): void => {
|
||||
if (requestQueues[topicId]) {
|
||||
requestQueues[topicId].clear()
|
||||
delete requestQueues[topicId]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all topic queues
|
||||
*/
|
||||
export const clearAllQueues = (): void => {
|
||||
Object.keys(requestQueues).forEach((topicId) => {
|
||||
requestQueues[topicId].clear()
|
||||
delete requestQueues[topicId]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a topic has pending requests
|
||||
* @param topicId The ID of the topic
|
||||
* @returns True if the topic has pending requests
|
||||
*/
|
||||
export const hasTopicPendingRequests = (topicId: string): boolean => {
|
||||
return requestQueues[topicId]?.size > 0 || requestQueues[topicId]?.pending > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of pending requests for a topic
|
||||
* @param topicId The ID of the topic
|
||||
* @returns The number of pending requests
|
||||
*/
|
||||
export const getTopicPendingRequestCount = (topicId: string): number => {
|
||||
if (!requestQueues[topicId]) {
|
||||
return 0
|
||||
}
|
||||
return requestQueues[topicId].size + requestQueues[topicId].pending
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all pending requests in a topic queue to complete
|
||||
* @param topicId The ID of the topic
|
||||
*/
|
||||
export const waitForTopicQueue = async (topicId: string): Promise<void> => {
|
||||
if (requestQueues[topicId]) {
|
||||
await requestQueues[topicId].onIdle()
|
||||
}
|
||||
}
|
||||
148
yarn.lock
148
yarn.lock
@ -2933,11 +2933,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.4":
|
||||
version: 8.5.14
|
||||
resolution: "@types/ws@npm:8.5.14"
|
||||
version: 8.18.0
|
||||
resolution: "@types/ws@npm:8.18.0"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/be88a0b6252f939cb83340bd1b4d450287f752c19271195cd97564fd94047259a9bb8c31c585a61b69d8a1b069a99df9dd804db0132d3359c54d3890c501416a
|
||||
checksum: 10c0/a56d2e0d1da7411a1f3548ce02b51a50cbe9e23f025677d03df48f87e4a3c72e1342fbf1d12e487d7eafa8dc670c605152b61bbf9165891ec0e9694b0d3ea8d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -3184,6 +3184,7 @@ __metadata:
|
||||
mime: "npm:^4.0.4"
|
||||
officeparser: "npm:^4.1.1"
|
||||
openai: "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
|
||||
p-queue: "npm:^8.1.0"
|
||||
prettier: "npm:^3.2.4"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
@ -3262,11 +3263,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.9.0":
|
||||
version: 8.14.0
|
||||
resolution: "acorn@npm:8.14.0"
|
||||
version: 8.14.1
|
||||
resolution: "acorn@npm:8.14.1"
|
||||
bin:
|
||||
acorn: bin/acorn
|
||||
checksum: 10c0/6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7
|
||||
checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -3420,8 +3421,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"antd@npm:^5.22.5":
|
||||
version: 5.24.2
|
||||
resolution: "antd@npm:5.24.2"
|
||||
version: 5.24.3
|
||||
resolution: "antd@npm:5.24.3"
|
||||
dependencies:
|
||||
"@ant-design/colors": "npm:^7.2.0"
|
||||
"@ant-design/cssinjs": "npm:^1.23.0"
|
||||
@ -3438,7 +3439,7 @@ __metadata:
|
||||
classnames: "npm:^2.5.1"
|
||||
copy-to-clipboard: "npm:^3.3.3"
|
||||
dayjs: "npm:^1.11.11"
|
||||
rc-cascader: "npm:~3.33.0"
|
||||
rc-cascader: "npm:~3.33.1"
|
||||
rc-checkbox: "npm:~3.5.0"
|
||||
rc-collapse: "npm:~3.9.0"
|
||||
rc-dialog: "npm:~9.6.0"
|
||||
@ -3446,14 +3447,14 @@ __metadata:
|
||||
rc-dropdown: "npm:~4.2.1"
|
||||
rc-field-form: "npm:~2.7.0"
|
||||
rc-image: "npm:~7.11.0"
|
||||
rc-input: "npm:~1.7.2"
|
||||
rc-input: "npm:~1.7.3"
|
||||
rc-input-number: "npm:~9.4.0"
|
||||
rc-mentions: "npm:~2.19.1"
|
||||
rc-menu: "npm:~9.16.1"
|
||||
rc-motion: "npm:^2.9.5"
|
||||
rc-notification: "npm:~5.6.3"
|
||||
rc-pagination: "npm:~5.1.0"
|
||||
rc-picker: "npm:~4.11.2"
|
||||
rc-picker: "npm:~4.11.3"
|
||||
rc-progress: "npm:~4.0.0"
|
||||
rc-rate: "npm:~2.13.1"
|
||||
rc-resize-observer: "npm:^1.4.3"
|
||||
@ -3466,7 +3467,7 @@ __metadata:
|
||||
rc-tabs: "npm:~15.5.1"
|
||||
rc-textarea: "npm:~1.9.0"
|
||||
rc-tooltip: "npm:~6.4.0"
|
||||
rc-tree: "npm:~5.13.0"
|
||||
rc-tree: "npm:~5.13.1"
|
||||
rc-tree-select: "npm:~5.27.0"
|
||||
rc-upload: "npm:~4.8.1"
|
||||
rc-util: "npm:^5.44.4"
|
||||
@ -3475,7 +3476,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: ">=16.9.0"
|
||||
react-dom: ">=16.9.0"
|
||||
checksum: 10c0/a6772136b828ef73925af633dee66b7580bad736eac20c713d1991e457aae3879d1a826ca06cc40f824238da0ad1a9745dbde6acbd801aec589deaeef04846fe
|
||||
checksum: 10c0/bbd1dcc4cce3bebecebde135014352156d57c8979b1ad8cb8cf873e8919454292a812f6d2e8995673b82f3cb46a2cdd14ea7b2190193bd5127380c1091ce472a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -4926,15 +4927,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:2.6.9, debug@npm:^2.6.9":
|
||||
version: 2.6.9
|
||||
resolution: "debug@npm:2.6.9"
|
||||
dependencies:
|
||||
ms: "npm:2.0.0"
|
||||
checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "debug@npm:4.4.0"
|
||||
@ -4959,6 +4951,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:^2.6.9":
|
||||
version: 2.6.9
|
||||
resolution: "debug@npm:2.6.9"
|
||||
dependencies:
|
||||
ms: "npm:2.0.0"
|
||||
checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:^3.1.0, debug@npm:^3.2.6":
|
||||
version: 3.2.7
|
||||
resolution: "debug@npm:3.2.7"
|
||||
@ -5533,9 +5534,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"electron-log@npm:^5.1.5":
|
||||
version: 5.3.1
|
||||
resolution: "electron-log@npm:5.3.1"
|
||||
checksum: 10c0/051157400b36f4ad51c52ae30a6c37d0c1525dc582312dc4043723d0e52aa11a95c28ee1421a0067f11ad641624e1163850e24d1c8cec47ba31aaf905953100f
|
||||
version: 5.3.2
|
||||
resolution: "electron-log@npm:5.3.2"
|
||||
checksum: 10c0/8cfcd6eb6ab2dff010941f9a39793a411646dc0462991632d2427fc9a45f22b00ec758996629f8c203612d99cb01713c08c72d2b0454603d13386f433f5b755b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5565,9 +5566,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"electron-to-chromium@npm:^1.5.73":
|
||||
version: 1.5.112
|
||||
resolution: "electron-to-chromium@npm:1.5.112"
|
||||
checksum: 10c0/fc597268d6d3d7458b55141c436802a6c51078855f021823cdb380b80ad1a69e1c2899fdfc9cffa501d47feb3791ea6a75893fe802a608c7845e979a48f5ac25
|
||||
version: 1.5.113
|
||||
resolution: "electron-to-chromium@npm:1.5.113"
|
||||
checksum: 10c0/837fe2fd26adbc4f3ad8e758d14067a14f636f9c2923b5ded8adb93426bbe3fdc83b48ddf9f2cf03be31b5becb0c31144db19c823b696fd52a7bc4583f4bde00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5681,13 +5682,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encodeurl@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "encodeurl@npm:1.0.2"
|
||||
checksum: 10c0/f6c2387379a9e7c1156c1c3d4f9cb7bb11cf16dd4c1682e1f6746512564b053df5781029b6061296832b59fb22f459dbe250386d217c2f6e203601abb2ee0bec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encoding@npm:^0.1.12, encoding@npm:^0.1.13":
|
||||
version: 0.1.13
|
||||
resolution: "encoding@npm:0.1.13"
|
||||
@ -6260,6 +6254,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "eventemitter3@npm:5.0.1"
|
||||
checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"events@npm:^3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "events@npm:3.3.0"
|
||||
@ -6602,17 +6603,16 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"finalhandler@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "finalhandler@npm:2.0.0"
|
||||
version: 2.1.0
|
||||
resolution: "finalhandler@npm:2.1.0"
|
||||
dependencies:
|
||||
debug: "npm:2.6.9"
|
||||
encodeurl: "npm:~1.0.2"
|
||||
escape-html: "npm:~1.0.3"
|
||||
on-finished: "npm:2.4.1"
|
||||
parseurl: "npm:~1.3.3"
|
||||
statuses: "npm:2.0.1"
|
||||
unpipe: "npm:~1.0.0"
|
||||
checksum: 10c0/ca6f69d69797eebc900d7627bde4bb7d38417112911eb11ce4e40011195b6ad1a09413ad082da9bb64da789a4ecfffdd0e6a5ea1ccb4147062224c3050f134ea
|
||||
debug: "npm:^4.4.0"
|
||||
encodeurl: "npm:^2.0.0"
|
||||
escape-html: "npm:^1.0.3"
|
||||
on-finished: "npm:^2.4.1"
|
||||
parseurl: "npm:^1.3.3"
|
||||
statuses: "npm:^2.0.1"
|
||||
checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -7478,8 +7478,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"hast-util-to-jsx-runtime@npm:^2.0.0":
|
||||
version: 2.3.5
|
||||
resolution: "hast-util-to-jsx-runtime@npm:2.3.5"
|
||||
version: 2.3.6
|
||||
resolution: "hast-util-to-jsx-runtime@npm:2.3.6"
|
||||
dependencies:
|
||||
"@types/estree": "npm:^1.0.0"
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
@ -7493,10 +7493,10 @@ __metadata:
|
||||
mdast-util-mdxjs-esm: "npm:^2.0.0"
|
||||
property-information: "npm:^7.0.0"
|
||||
space-separated-tokens: "npm:^2.0.0"
|
||||
style-to-object: "npm:^1.0.0"
|
||||
style-to-js: "npm:^1.0.0"
|
||||
unist-util-position: "npm:^5.0.0"
|
||||
vfile-message: "npm:^4.0.0"
|
||||
checksum: 10c0/9db65b2b417cdaad1f1cc619b613abd8d1fa7196f5979ce54bd1dc8a937613f11fecb8b7a43425342cf36fd085b0fed89daadcce43bed8762786a4cdc21a1df8
|
||||
checksum: 10c0/27297e02848fe37ef219be04a26ce708d17278a175a807689e94a821dcffc88aa506d62c3a85beed1f9a8544f7211bdcbcde0528b7b456a57c2e342c3fd11056
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10979,6 +10979,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-queue@npm:^8.1.0":
|
||||
version: 8.1.0
|
||||
resolution: "p-queue@npm:8.1.0"
|
||||
dependencies:
|
||||
eventemitter3: "npm:^5.0.1"
|
||||
p-timeout: "npm:^6.1.2"
|
||||
checksum: 10c0/6bdea170840546769c29682fed212745c951933476761ed3a981967fab624c7c0120dff79bd99a1ac8b650b420719a245813e944af4b8ee77d4dd78adbf5fe75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-retry@npm:4":
|
||||
version: 4.6.2
|
||||
resolution: "p-retry@npm:4.6.2"
|
||||
@ -11005,6 +11015,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-timeout@npm:^6.1.2":
|
||||
version: 6.1.4
|
||||
resolution: "p-timeout@npm:6.1.4"
|
||||
checksum: 10c0/019edad1c649ab07552aa456e40ce7575c4b8ae863191477f02ac8d283ac8c66cedef0ca93422735130477a051dfe952ba717641673fd3599befdd13f63bcc33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-try@npm:^2.0.0":
|
||||
version: 2.2.0
|
||||
resolution: "p-try@npm:2.2.0"
|
||||
@ -11691,7 +11708,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc-cascader@npm:~3.33.0":
|
||||
"rc-cascader@npm:~3.33.1":
|
||||
version: 3.33.1
|
||||
resolution: "rc-cascader@npm:3.33.1"
|
||||
dependencies:
|
||||
@ -11830,7 +11847,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc-input@npm:~1.7.1, rc-input@npm:~1.7.2":
|
||||
"rc-input@npm:~1.7.1, rc-input@npm:~1.7.3":
|
||||
version: 1.7.3
|
||||
resolution: "rc-input@npm:1.7.3"
|
||||
dependencies:
|
||||
@ -11937,7 +11954,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc-picker@npm:~4.11.2":
|
||||
"rc-picker@npm:~4.11.3":
|
||||
version: 4.11.3
|
||||
resolution: "rc-picker@npm:4.11.3"
|
||||
dependencies:
|
||||
@ -12086,8 +12103,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"rc-table@npm:~7.50.3":
|
||||
version: 7.50.3
|
||||
resolution: "rc-table@npm:7.50.3"
|
||||
version: 7.50.4
|
||||
resolution: "rc-table@npm:7.50.4"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.10.1"
|
||||
"@rc-component/context": "npm:^1.4.0"
|
||||
@ -12098,7 +12115,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: ">=16.9.0"
|
||||
react-dom: ">=16.9.0"
|
||||
checksum: 10c0/61fee18289063d33e135f87e7d325c4ab319db6a452bc7151b13c9bfdcedc9a280a543fe06dea05cd41814c47dfc0c5b3ade876f7e6c286714a8276a53f7125b
|
||||
checksum: 10c0/ab5eb3db00bc31470d7dd5946c1a919247742703a3278ea2d9a33e719a08e170ca80977e615084c7cfcb54508a9c3da2449409cce16fc1d7ec1ea67596c30d79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -12167,7 +12184,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc-tree@npm:~5.13.0":
|
||||
"rc-tree@npm:~5.13.0, rc-tree@npm:~5.13.1":
|
||||
version: 5.13.1
|
||||
resolution: "rc-tree@npm:5.13.1"
|
||||
dependencies:
|
||||
@ -12211,8 +12228,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"rc-virtual-list@npm:^3.14.2, rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2":
|
||||
version: 3.18.3
|
||||
resolution: "rc-virtual-list@npm:3.18.3"
|
||||
version: 3.18.4
|
||||
resolution: "rc-virtual-list@npm:3.18.4"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.20.0"
|
||||
classnames: "npm:^2.2.6"
|
||||
@ -12221,7 +12238,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: ">=16.9.0"
|
||||
react-dom: ">=16.9.0"
|
||||
checksum: 10c0/773b18d9594d78b4f08182caec3043fef6c1b1871db91e05294b62a65e309e2d0ee50ca699a1b237af1ac0b1b1c9cc25d62f47bae30d4a0c0dcab0ddc4d5bb8d
|
||||
checksum: 10c0/2566e98418b8072af4591488e028a1e8433d2a190716f47134ee63a506afec9cf472f8a8d3dda55bf2e82ccf1215d324abd280892db4fbf5999fc30184068f62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -13961,7 +13978,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"style-to-object@npm:^1.0.0":
|
||||
"style-to-js@npm:^1.0.0":
|
||||
version: 1.1.16
|
||||
resolution: "style-to-js@npm:1.1.16"
|
||||
dependencies:
|
||||
style-to-object: "npm:1.0.8"
|
||||
checksum: 10c0/578a4dff804539ec7e64d3cc8d327540befb9ad30e3cd0b6b0392f93f793f3a028f90084a9aaff088bffb87818fa2c6c153f0df576f61f9ab0b0938b582bcac7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"style-to-object@npm:1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "style-to-object@npm:1.0.8"
|
||||
dependencies:
|
||||
@ -14737,7 +14763,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
||||
"unpipe@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "unpipe@npm:1.0.0"
|
||||
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user