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-pdf',
|
||||||
'@llm-tools/embedjs-loader-sitemap',
|
'@llm-tools/embedjs-loader-sitemap',
|
||||||
'@llm-tools/embedjs-libsql',
|
'@llm-tools/embedjs-libsql',
|
||||||
'@llm-tools/embedjs-loader-image'
|
'@llm-tools/embedjs-loader-image',
|
||||||
|
'p-queue'
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
...visualizerPlugin('main')
|
...visualizerPlugin('main')
|
||||||
|
|||||||
@ -81,6 +81,7 @@
|
|||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
|
"p-queue": "^8.1.0",
|
||||||
"tokenx": "^0.4.1",
|
"tokenx": "^0.4.1",
|
||||||
"webdav": "4.11.4"
|
"webdav": "4.11.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,3 +12,7 @@ export const isWindows = platform === 'win32' || platform === 'win64'
|
|||||||
export const isLinux = platform === 'linux'
|
export const isLinux = platform === 'linux'
|
||||||
|
|
||||||
export const SILICON_CLIENT_ID = 'SFaJLLq0y6CAMoyDm81aMu'
|
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 db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { initializeMessagesState } from '@renderer/store/messages'
|
||||||
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
||||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
@ -25,6 +26,11 @@ export function useAppInit() {
|
|||||||
|
|
||||||
useFullScreenNotice()
|
useFullScreenNotice()
|
||||||
|
|
||||||
|
// Initialize messages state
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(initializeMessagesState())
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||||
}, [avatar, dispatch])
|
}, [avatar, dispatch])
|
||||||
|
|||||||
@ -41,28 +41,34 @@ export async function getTopicById(topicId: string) {
|
|||||||
return { ...topic, messages } as Topic
|
return { ...topic, messages } as Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TopicManager {
|
// Convert class to object with functions since class only has static methods
|
||||||
static async getTopic(id: string) {
|
// 只有静态方法,没必要用class
|
||||||
|
export const TopicManager = {
|
||||||
|
async getTopic(id: string) {
|
||||||
return await db.topics.get(id)
|
return await db.topics.get(id)
|
||||||
}
|
},
|
||||||
|
|
||||||
static async getTopicMessages(id: string) {
|
async getAllTopics() {
|
||||||
const topic = await this.getTopic(id)
|
return await db.topics.toArray()
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTopicMessages(id: string) {
|
||||||
|
const topic = await TopicManager.getTopic(id)
|
||||||
return topic ? topic.messages : []
|
return topic ? topic.messages : []
|
||||||
}
|
},
|
||||||
|
|
||||||
static async removeTopic(id: string) {
|
async removeTopic(id: string) {
|
||||||
const messages = await this.getTopicMessages(id)
|
const messages = await TopicManager.getTopicMessages(id)
|
||||||
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
await deleteMessageFiles(message)
|
await deleteMessageFiles(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.topics.delete(id)
|
db.topics.delete(id)
|
||||||
}
|
},
|
||||||
|
|
||||||
static async clearTopicMessages(id: string) {
|
async clearTopicMessages(id: string) {
|
||||||
const topic = await this.getTopic(id)
|
const topic = await TopicManager.getTopic(id)
|
||||||
|
|
||||||
if (topic) {
|
if (topic) {
|
||||||
for (const message of topic?.messages ?? []) {
|
for (const message of topic?.messages ?? []) {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const Chat: FC<Props> = (props) => {
|
|||||||
topic={props.activeTopic}
|
topic={props.activeTopic}
|
||||||
setActiveTopic={props.setActiveTopic}
|
setActiveTopic={props.setActiveTopic}
|
||||||
/>
|
/>
|
||||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
|
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||||
</Main>
|
</Main>
|
||||||
{topicPosition === 'right' && showTopics && (
|
{topicPosition === 'right' && showTopics && (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@ -25,15 +25,15 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
|
|||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import WebSearchService from '@renderer/services/WebSearchService'
|
import WebSearchService from '@renderer/services/WebSearchService'
|
||||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { sendMessage as _sendMessage } from '@renderer/store/messages'
|
||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||||
import { Assistant, FileType, KnowledgeBase, MCPServer, Message, Model, Topic } from '@renderer/types'
|
import { Assistant, FileType, KnowledgeBase, MCPServer, MCPServer, Message, Model, Topic } from '@renderer/types'
|
||||||
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
import { classNames, delay, getFileExtension } from '@renderer/utils'
|
||||||
import { abortCompletion } from '@renderer/utils/abortController'
|
import { abortCompletion } from '@renderer/utils/abortController'
|
||||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import Logger from 'electron-log/renderer'
|
import Logger from 'electron-log/renderer'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
import { debounce, isEmpty } from 'lodash'
|
||||||
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@ -50,10 +50,10 @@ import MentionModelsButton from './MentionModelsButton'
|
|||||||
import MentionModelsInput from './MentionModelsInput'
|
import MentionModelsInput from './MentionModelsInput'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
setActiveTopic: (topic: Topic) => void
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
topic: Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
@ -137,43 +137,28 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const message: Message = {
|
try {
|
||||||
id: uuid(),
|
// Dispatch the sendMessage action with all options
|
||||||
role: 'user',
|
const uploadedFiles = await FileManager.uploadFiles(files)
|
||||||
content: text,
|
dispatch(
|
||||||
assistantId: assistant.id,
|
_sendMessage(text, assistant, assistant.topics[0], {
|
||||||
topicId: assistant.topics[0].id || uuid(),
|
files: uploadedFiles,
|
||||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
knowledgeBaseIds: selectedKnowledgeBases?.map((base) => base.id),
|
||||||
type: 'text',
|
mentionModels,
|
||||||
status: 'success'
|
enabledMCPs
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
setText('')
|
||||||
|
setFiles([])
|
||||||
|
setTimeout(() => setText(''), 500)
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
setExpend(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send message:', error)
|
||||||
}
|
}
|
||||||
|
}, [inputEmpty, text, assistant, files, selectedKnowledgeBases, mentionModels, dispatch])
|
||||||
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])
|
|
||||||
|
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
if (isTranslating) {
|
if (isTranslating) {
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||||
import db from '@renderer/databases'
|
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useModel } from '@renderer/hooks/useModel'
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
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 { 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 { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
|
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||||
import { Message, Topic } from '@renderer/types'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { updateMessages } from '@renderer/store/messages'
|
||||||
|
import { Assistant, Message, Topic } from '@renderer/types'
|
||||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||||
import { Divider, Dropdown } from 'antd'
|
import { Divider, Dropdown } from 'antd'
|
||||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@ -24,44 +23,46 @@ import MessageTokens from './MessageTokens'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
topic?: Topic
|
topic: Topic
|
||||||
|
assistant?: Assistant
|
||||||
index?: number
|
index?: number
|
||||||
total?: number
|
total?: number
|
||||||
hidePresetMessages?: boolean
|
hidePresetMessages?: boolean
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
isGrouped?: boolean
|
isGrouped?: boolean
|
||||||
|
isStreaming?: boolean
|
||||||
onGetMessages?: () => Message[]
|
onGetMessages?: () => Message[]
|
||||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||||
onDeleteMessage?: (message: Message) => Promise<void>
|
onDeleteMessage?: (message: Message) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({
|
const MessageItem: FC<Props> = ({
|
||||||
message: _message,
|
message,
|
||||||
topic: _topic,
|
topic,
|
||||||
|
// assistant,
|
||||||
index,
|
index,
|
||||||
hidePresetMessages,
|
hidePresetMessages,
|
||||||
isGrouped,
|
isGrouped,
|
||||||
|
isStreaming = false,
|
||||||
style,
|
style,
|
||||||
onDeleteMessage,
|
onDeleteMessage,
|
||||||
onSetMessages,
|
|
||||||
onGetMessages
|
onGetMessages
|
||||||
}) => {
|
}) => {
|
||||||
const [message, setMessage] = useState(_message)
|
const dispatch = useAppDispatch()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { isBubbleStyle } = useMessageStyle()
|
const { isBubbleStyle } = useMessageStyle()
|
||||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
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 [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
|
||||||
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
|
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
|
||||||
const [selectedText, setSelectedText] = useState<string>('')
|
const [selectedText, setSelectedText] = useState<string>('')
|
||||||
|
|
||||||
const isLastMessage = index === 0
|
const isLastMessage = index === 0
|
||||||
const isAssistantMessage = message.role === 'assistant'
|
const isAssistantMessage = message.role === 'assistant'
|
||||||
|
const showMenubar = !isStreaming && !message.status.includes('ing')
|
||||||
const showMenubar = !message.status.includes('ing')
|
|
||||||
|
|
||||||
const fontFamily = useMemo(() => {
|
const fontFamily = useMemo(() => {
|
||||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||||
@ -95,28 +96,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onEditMessage = useCallback(
|
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
|
||||||
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) => {
|
|
||||||
if (messageContainerRef.current) {
|
if (messageContainerRef.current) {
|
||||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
@ -127,62 +107,25 @@ const MessageItem: FC<Props> = ({
|
|||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
|
||||||
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler),
|
|
||||||
EventEmitter.on(EVENT_NAMES.RESEND_MESSAGE + ':' + message.id, onEditMessage)
|
|
||||||
]
|
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [message, onEditMessage])
|
}, [message.id, messageHighlightHandler])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message.role === 'user' && !message.usage) {
|
if (message.role === 'user' && !message.usage && topic) {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
const usage = await estimateMessageUsage(message)
|
const usage = await estimateMessageUsage(message)
|
||||||
setMessage({ ...message, usage })
|
if (topic) {
|
||||||
const topic = await db.topics.get({ id: message.topicId })
|
await dispatch(
|
||||||
const messages = topic?.messages.map((m) => (m.id === message.id ? { ...m, usage } : m))
|
updateMessages(topic, onGetMessages?.()?.map((m) => (m.id === message.id ? { ...m, usage } : m)) || [])
|
||||||
db.topics.update(message.topicId, { messages })
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [message])
|
}, [message, topic, dispatch, onGetMessages])
|
||||||
|
|
||||||
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])
|
|
||||||
|
|
||||||
if (hidePresetMessages && message.isPreset) {
|
if (hidePresetMessages && message.isPreset) {
|
||||||
return null
|
return null
|
||||||
@ -235,15 +178,15 @@ const MessageItem: FC<Props> = ({
|
|||||||
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
||||||
<MessageMenubar
|
<MessageMenubar
|
||||||
message={message}
|
message={message}
|
||||||
assistantModel={assistant?.model}
|
assistant={assistant}
|
||||||
model={model}
|
model={model}
|
||||||
index={index}
|
index={index}
|
||||||
|
topic={topic}
|
||||||
isLastMessage={isLastMessage}
|
isLastMessage={isLastMessage}
|
||||||
isAssistantMessage={isAssistantMessage}
|
isAssistantMessage={isAssistantMessage}
|
||||||
isGrouped={isGrouped}
|
isGrouped={isGrouped}
|
||||||
messageContainerRef={messageContainerRef}
|
messageContainerRef={messageContainerRef}
|
||||||
setModel={setModel}
|
setModel={setModel}
|
||||||
onEditMessage={onEditMessage}
|
|
||||||
onDeleteMessage={onDeleteMessage}
|
onDeleteMessage={onDeleteMessage}
|
||||||
onGetMessages={onGetMessages}
|
onGetMessages={onGetMessages}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
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 { classNames } from '@renderer/utils'
|
||||||
import { Popover } from 'antd'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
import MessageItem from './Message'
|
|
||||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||||
|
import MessageStream from './MessageStream'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: (Message & { index: number })[]
|
messages: (Message & { index: number })[]
|
||||||
topic?: Topic
|
topic: Topic
|
||||||
hidePresetMessages?: boolean
|
hidePresetMessages?: boolean
|
||||||
onGetMessages?: () => Message[]
|
onGetMessages: () => Message[]
|
||||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
onSetMessages: Dispatch<SetStateAction<Message[]>>
|
||||||
onDeleteMessage?: (message: Message) => Promise<void>
|
onDeleteMessage: (message: Message) => Promise<void>
|
||||||
onDeleteGroupMessages?: (askId: string) => Promise<void>
|
onDeleteGroupMessages: (askId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageGroup: FC<Props> = ({
|
const MessageGroup = ({
|
||||||
messages,
|
messages,
|
||||||
topic,
|
topic,
|
||||||
hidePresetMessages,
|
hidePresetMessages,
|
||||||
@ -29,7 +30,7 @@ const MessageGroup: FC<Props> = ({
|
|||||||
onSetMessages,
|
onSetMessages,
|
||||||
onGetMessages,
|
onGetMessages,
|
||||||
onDeleteGroupMessages
|
onDeleteGroupMessages
|
||||||
}) => {
|
}: Props) => {
|
||||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -43,7 +44,10 @@ const MessageGroup: FC<Props> = ({
|
|||||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||||
const isGrid = multiModelMessageStyle === 'grid'
|
const isGrid = multiModelMessageStyle === 'grid'
|
||||||
|
|
||||||
const onDelete = useCallback(async () => {
|
const handleDeleteGroup = useCallback(async () => {
|
||||||
|
const askId = messages[0]?.askId
|
||||||
|
if (!askId) return
|
||||||
|
|
||||||
window.modal.confirm({
|
window.modal.confirm({
|
||||||
title: t('message.group.delete.title'),
|
title: t('message.group.delete.title'),
|
||||||
content: t('message.group.delete.content'),
|
content: t('message.group.delete.content'),
|
||||||
@ -52,10 +56,7 @@ const MessageGroup: FC<Props> = ({
|
|||||||
danger: true
|
danger: true
|
||||||
},
|
},
|
||||||
okText: t('common.delete'),
|
okText: t('common.delete'),
|
||||||
onOk: () => {
|
onOk: () => onDeleteGroupMessages(askId)
|
||||||
const askId = messages[0].askId
|
|
||||||
askId && onDeleteGroupMessages?.(askId)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}, [messages, onDeleteGroupMessages, t])
|
}, [messages, onDeleteGroupMessages, t])
|
||||||
|
|
||||||
@ -63,6 +64,72 @@ const MessageGroup: FC<Props> = ({
|
|||||||
setSelectedIndex(messageLength - 1)
|
setSelectedIndex(messageLength - 1)
|
||||||
}, [messageLength])
|
}, [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 (
|
return (
|
||||||
<GroupContainer
|
<GroupContainer
|
||||||
$isGrouped={isGrouped}
|
$isGrouped={isGrouped}
|
||||||
@ -73,86 +140,7 @@ const MessageGroup: FC<Props> = ({
|
|||||||
$layout={multiModelMessageStyle}
|
$layout={multiModelMessageStyle}
|
||||||
$gridColumns={gridColumns}
|
$gridColumns={gridColumns}
|
||||||
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||||
{messages.map((message, index) => {
|
{messages.map((message, index) => renderMessage(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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</GridContainer>
|
</GridContainer>
|
||||||
{isGrouped && (
|
{isGrouped && (
|
||||||
<MessageGroupMenuBar
|
<MessageGroupMenuBar
|
||||||
@ -161,7 +149,7 @@ const MessageGroup: FC<Props> = ({
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
selectedIndex={selectedIndex}
|
selectedIndex={selectedIndex}
|
||||||
setSelectedIndex={setSelectedIndex}
|
setSelectedIndex={setSelectedIndex}
|
||||||
onDelete={onDelete}
|
onDelete={handleDeleteGroup}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</GroupContainer>
|
</GroupContainer>
|
||||||
|
|||||||
@ -19,13 +19,18 @@ import { modelGenerating } from '@renderer/hooks/useRuntime'
|
|||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
import { getMessageTitle, resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||||
import { translateText } from '@renderer/services/TranslateService'
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
captureScrollableDivAsBlob,
|
clearStreamMessage,
|
||||||
captureScrollableDivAsDataURL,
|
commitStreamMessage,
|
||||||
removeTrailingDoubleSpaces,
|
resendMessage,
|
||||||
uuid
|
setStreamMessage,
|
||||||
} from '@renderer/utils'
|
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 {
|
import {
|
||||||
exportMarkdownToNotion,
|
exportMarkdownToNotion,
|
||||||
exportMarkdownToYuque,
|
exportMarkdownToYuque,
|
||||||
@ -35,21 +40,21 @@ import {
|
|||||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { isEmpty } from 'lodash'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
assistantModel?: Model
|
assistant: Assistant
|
||||||
model?: Model
|
topic: Topic
|
||||||
|
model: Model
|
||||||
index?: number
|
index?: number
|
||||||
isGrouped?: boolean
|
isGrouped?: boolean
|
||||||
isLastMessage: boolean
|
isLastMessage: boolean
|
||||||
isAssistantMessage: boolean
|
isAssistantMessage: boolean
|
||||||
messageContainerRef: React.RefObject<HTMLDivElement>
|
messageContainerRef: React.RefObject<HTMLDivElement>
|
||||||
setModel: (model: Model) => void
|
setModel: (model: Model) => void
|
||||||
onEditMessage?: (message: Message) => void
|
|
||||||
onDeleteMessage?: (message: Message) => Promise<void>
|
onDeleteMessage?: (message: Message) => Promise<void>
|
||||||
onGetMessages?: () => Message[]
|
onGetMessages?: () => Message[]
|
||||||
}
|
}
|
||||||
@ -59,18 +64,21 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
message,
|
message,
|
||||||
index,
|
index,
|
||||||
isGrouped,
|
isGrouped,
|
||||||
model,
|
|
||||||
isLastMessage,
|
isLastMessage,
|
||||||
isAssistantMessage,
|
isAssistantMessage,
|
||||||
assistantModel,
|
assistant,
|
||||||
|
topic,
|
||||||
|
model,
|
||||||
messageContainerRef,
|
messageContainerRef,
|
||||||
onEditMessage,
|
|
||||||
onDeleteMessage,
|
onDeleteMessage,
|
||||||
onGetMessages
|
onGetMessages
|
||||||
} = props
|
} = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [isTranslating, setIsTranslating] = 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'
|
const isUserMessage = message.role === 'user'
|
||||||
|
|
||||||
@ -88,50 +96,33 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const onNewBranch = useCallback(async () => {
|
const onNewBranch = useCallback(async () => {
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||||
window.message.success({
|
window.message.success({ content: t('chat.message.new.branch.created'), key: 'new-branch' })
|
||||||
content: t('chat.message.new.branch.created'),
|
|
||||||
key: 'new-branch'
|
|
||||||
})
|
|
||||||
}, [index, t])
|
}, [index, t])
|
||||||
|
|
||||||
const onResend = useCallback(async () => {
|
const handleResendUserMessage = useCallback(
|
||||||
await modelGenerating()
|
async (messageUpdate?: Message) => {
|
||||||
const _messages = onGetMessages?.() || []
|
// messageUpdate 为了处理用户消息更改后的message
|
||||||
const groupdMessages = _messages.filter((m) => m.askId === message.id)
|
await modelGenerating()
|
||||||
|
const groupdMessages = messages.filter((m) => m.askId === message.id)
|
||||||
|
|
||||||
// Resend all groupd messages
|
// Resend all grouped messages
|
||||||
if (!isEmpty(groupdMessages)) {
|
if (!isEmpty(groupdMessages)) {
|
||||||
for (const assistantMessage of groupdMessages) {
|
for (const assistantMessage of groupdMessages) {
|
||||||
const _model = assistantMessage.model || assistantModel
|
const _model = assistantMessage.model || assistantModel
|
||||||
EventEmitter.emit(
|
await dispatch(resendMessage({ ...assistantMessage, model: _model }, assistant, topic))
|
||||||
EVENT_NAMES.RESEND_MESSAGE + ':' + assistantMessage.id,
|
}
|
||||||
resetAssistantMessage(assistantMessage, _model)
|
return
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no groupd message, resend next message
|
await dispatch(resendMessage(messageUpdate ?? message, assistant, topic))
|
||||||
const index = _messages.findIndex((m) => m.id === message.id)
|
},
|
||||||
const nextIndex = index + 1
|
[message, assistantModel, model, onDeleteMessage, onGetMessages, dispatch, assistant, topic]
|
||||||
const nextMessage = _messages[nextIndex]
|
)
|
||||||
|
|
||||||
if (nextMessage && nextMessage.role === 'assistant') {
|
// const onResendUserMessage = useCallback(async () => {
|
||||||
EventEmitter.emit(EVENT_NAMES.RESEND_MESSAGE + ':' + nextMessage.id, {
|
// // await dispatch(resendMessage(message, assistant, topic))
|
||||||
...nextMessage,
|
// onResend()
|
||||||
content: '',
|
// }, [message, dispatch, assistant, topic])
|
||||||
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 onEdit = useCallback(async () => {
|
const onEdit = useCallback(async () => {
|
||||||
let resendMessage = false
|
let resendMessage = false
|
||||||
@ -152,43 +143,54 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (editedText && editedText !== message.content) {
|
||||||
|
// 同步修改store中用户消息
|
||||||
|
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { content: editedText } }))
|
||||||
|
|
||||||
if (editedText) {
|
// const updatedMessages = onGetMessages?.() || []
|
||||||
await onEditMessage?.({ ...message, content: editedText })
|
// dispatch(updateMessages(topic, updatedMessages))
|
||||||
}
|
}
|
||||||
|
|
||||||
resendMessage && onResend()
|
if (resendMessage) handleResendUserMessage({ ...message, content: editedText })
|
||||||
}, [message, onEditMessage, onResend, t])
|
}, [message, dispatch, topic, onGetMessages, handleResendUserMessage, t])
|
||||||
|
|
||||||
const onResendUserMessage = useCallback(async () => {
|
|
||||||
await onEditMessage?.({ ...message, content: message.content })
|
|
||||||
onResend && onResend()
|
|
||||||
}, [message, onEditMessage, onResend])
|
|
||||||
|
|
||||||
const handleTranslate = useCallback(
|
const handleTranslate = useCallback(
|
||||||
async (language: string) => {
|
async (language: string) => {
|
||||||
if (isTranslating) return
|
if (isTranslating) return
|
||||||
|
|
||||||
onEditMessage?.({ ...message, translatedContent: t('translate.processing') })
|
dispatch(
|
||||||
|
updateMessage({
|
||||||
|
topicId: topic.id,
|
||||||
|
messageId: message.id,
|
||||||
|
updates: { translatedContent: t('translate.processing') }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
setIsTranslating(true)
|
setIsTranslating(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await translateText(message.content, language, (text) =>
|
await translateText(message.content, language, (text) => {
|
||||||
onEditMessage?.({ ...message, translatedContent: text })
|
// 使用 setStreamMessage 来更新翻译内容
|
||||||
)
|
dispatch(
|
||||||
|
setStreamMessage({
|
||||||
|
topicId: topic.id,
|
||||||
|
message: { ...message, translatedContent: text }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 翻译完成后,提交流消息
|
||||||
|
dispatch(commitStreamMessage({ topicId: topic.id }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Translation failed:', error)
|
console.error('Translation failed:', error)
|
||||||
window.message.error({
|
window.message.error({ content: t('translate.error.failed'), key: 'translate-message' })
|
||||||
content: t('translate.error.failed'),
|
dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: { translatedContent: undefined } }))
|
||||||
key: 'translate-message'
|
dispatch(clearStreamMessage({ topicId: topic.id }))
|
||||||
})
|
|
||||||
onEditMessage?.({ ...message, translatedContent: undefined })
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isTranslating, message, onEditMessage, t]
|
[isTranslating, message, dispatch, topic, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
const dropdownItems = useMemo(
|
||||||
@ -202,18 +204,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
window.api.file.save(fileName, message.content)
|
window.api.file.save(fileName, message.content)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ label: t('common.edit'), key: 'edit', icon: <EditOutlined />, onClick: onEdit },
|
||||||
label: t('common.edit'),
|
{ label: t('chat.message.new.branch'), key: 'new-branch', icon: <ForkOutlined />, onClick: onNewBranch },
|
||||||
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'),
|
label: t('chat.topics.export.title'),
|
||||||
key: 'export',
|
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'),
|
label: t('chat.topics.export.word'),
|
||||||
@ -284,7 +272,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
const selectedModel = isGrouped ? model : assistantModel
|
const selectedModel = isGrouped ? model : assistantModel
|
||||||
const _message = resetAssistantMessage(message, selectedModel)
|
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) => {
|
const onMentionModel = async (e: React.MouseEvent) => {
|
||||||
@ -293,28 +282,24 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const selectedModel = await SelectModelPopup.show({ model })
|
const selectedModel = await SelectModelPopup.show({ model })
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
|
|
||||||
const _message: Message = resetAssistantMessage(message, selectedModel)
|
// const mentionModelMessage: Message = resetAssistantMessage(message, selectedModel)
|
||||||
|
// dispatch(updateMessage({ topicId: topic.id, messageId: message.id, updates: _message }))
|
||||||
if (message.askId && message.model) {
|
await dispatch(resendMessage(message, { ...assistant, model: selectedModel }, topic, true))
|
||||||
return EventEmitter.emit(EVENT_NAMES.APPEND_MESSAGE, { ..._message, id: uuid() })
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditMessage?.(_message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUseful = useCallback(
|
const onUseful = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
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 (
|
return (
|
||||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||||
<ActionButton className="message-action-button" onClick={onResendUserMessage}>
|
<ActionButton className="message-action-button" onClick={() => handleResendUserMessage()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -365,7 +350,14 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
{
|
{
|
||||||
label: '✖ ' + t('translate.close'),
|
label: '✖ ' + t('translate.close'),
|
||||||
key: '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()
|
onClick: (e) => e.domEvent.stopPropagation()
|
||||||
@ -467,4 +459,4 @@ const ReSendButton = styled(Button)`
|
|||||||
left: 0;
|
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 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 { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
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 { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import {
|
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
|
||||||
deleteMessageFiles,
|
|
||||||
getAssistantMessage,
|
|
||||||
getContextCount,
|
|
||||||
getGroupedMessages,
|
|
||||||
getUserMessage
|
|
||||||
} from '@renderer/services/MessagesService'
|
|
||||||
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
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 { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, runAsyncFunction } from '@renderer/utils'
|
||||||
import { t } from 'i18next'
|
import { last } from 'lodash'
|
||||||
import { flatten, last, take } from 'lodash'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useTranslation } from 'react-i18next'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
import BeatLoader from 'react-spinners/BeatLoader'
|
import BeatLoader from 'react-spinners/BeatLoader'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -29,29 +31,48 @@ import MessageGroup from './MessageGroup'
|
|||||||
import NarrowLayout from './NarrowLayout'
|
import NarrowLayout from './NarrowLayout'
|
||||||
import Prompt from './Prompt'
|
import Prompt from './Prompt'
|
||||||
|
|
||||||
interface Props {
|
interface MessagesProps {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
topic: Topic
|
topic: Topic
|
||||||
setActiveTopic: (topic: Topic) => void
|
setActiveTopic: (topic: Topic) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
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 [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
useEffect(() => {
|
||||||
const messagesRef = useRef(messages)
|
const reversedMessages = [...messages].reverse()
|
||||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
const newDisplayMessages = reversedMessages.slice(0, displayCount)
|
||||||
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
|
|
||||||
|
|
||||||
const groupedMessages = getGroupedMessages(displayMessages)
|
setDisplayMessages(newDisplayMessages)
|
||||||
|
setHasMore(messages.length > displayCount)
|
||||||
|
}, [messages, displayCount])
|
||||||
|
|
||||||
const INITIAL_MESSAGES_COUNT = 20
|
const handleDeleteMessage = useCallback(
|
||||||
const LOAD_MORE_COUNT = 20
|
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 maxWidth = useMemo(() => {
|
||||||
const showRightTopics = showTopics && topicPosition === 'right'
|
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)
|
setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 50)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
// const onAppendMessageMemo = useCallback(
|
||||||
async (message: Message) => {
|
// async (message: Message) => {
|
||||||
const assistantMessages: Message[] = []
|
// const newMessages = [...messages, message]
|
||||||
|
// await dispatch(updateMessages(topic, newMessages))
|
||||||
|
// },
|
||||||
|
// [topic, dispatch, messages]
|
||||||
|
// )
|
||||||
|
|
||||||
if (message.mentions?.length) {
|
const autoRenameTopicMemo = useCallback(async () => {
|
||||||
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 _topic = getTopic(assistant, topic.id)
|
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) {
|
if (!enableTopicNaming) {
|
||||||
const topicName = messages[0].content.substring(0, 50)
|
const topicName = messages[0]?.content.substring(0, 50)
|
||||||
const data = { ..._topic, name: topicName } as Topic
|
if (topicName) {
|
||||||
setActiveTopic(data)
|
const data = { ..._topic, name: topicName } as Topic
|
||||||
updateTopic(data)
|
setActiveTopic(data)
|
||||||
|
updateTopic(data)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto rename the topic
|
|
||||||
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||||
if (summaryText) {
|
if (summaryText) {
|
||||||
@ -124,60 +114,33 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
updateTopic(data)
|
updateTopic(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic])
|
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic, t])
|
||||||
|
|
||||||
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
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage),
|
// EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessageMemo),
|
||||||
EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessage),
|
// EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, () => {
|
||||||
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async () => {
|
// setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||||
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.AI_AUTO_RENAME, autoRenameTopicMemo),
|
||||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, (data: Topic) => {
|
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, async (data: Topic) => {
|
||||||
const defaultTopic = getDefaultTopic(assistant.id)
|
const defaultTopic = getDefaultTopic(assistant.id)
|
||||||
|
|
||||||
// Clear messages of other topics
|
|
||||||
if (data && data.id !== topic.id) {
|
if (data && data.id !== topic.id) {
|
||||||
TopicManager.clearTopicMessages(data.id)
|
await dispatch(clearTopicMessages(data.id))
|
||||||
updateTopic({ ...data, name: defaultTopic.name, messages: [] })
|
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear messages of current topic
|
await dispatch(clearTopicMessages(topic.id))
|
||||||
setMessages([])
|
|
||||||
setDisplayMessages([])
|
setDisplayMessages([])
|
||||||
const _topic = getTopic(assistant, topic.id)
|
const _topic = getTopic(assistant, topic.id)
|
||||||
_topic && updateTopic({ ..._topic, name: defaultTopic.name, messages: [] })
|
if (_topic) {
|
||||||
TopicManager.clearTopicMessages(topic.id)
|
updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
||||||
@ -192,68 +155,29 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
window.api.file.saveImage(topic.name, imageData)
|
window.api.file.saveImage(topic.name, imageData)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => {
|
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, async () => {
|
||||||
const lastMessage = last(messages)
|
const lastMessage = last(messages)
|
||||||
|
if (lastMessage?.type === 'clear') {
|
||||||
if (lastMessage && lastMessage.type === 'clear') {
|
handleDeleteMessage(lastMessage)
|
||||||
onDeleteMessage(lastMessage)
|
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages((prev) => {
|
|
||||||
const messages = prev.concat([getUserMessage({ assistant, topic, type: 'clear' })])
|
|
||||||
db.topics.put({ id: topic.id, messages })
|
|
||||||
return messages
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const clearMessage = getUserMessage({ assistant, topic, type: 'clear' })
|
||||||
|
const newMessages = [...messages, clearMessage]
|
||||||
|
await dispatch(updateMessages(topic, newMessages))
|
||||||
scrollToBottom()
|
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(() => {
|
return () => {
|
||||||
runAsyncFunction(async () => {
|
for (const unsub of unsubscribes) {
|
||||||
const messages = (await TopicManager.getTopicMessages(topic.id)) || []
|
unsub()
|
||||||
setMessages(messages)
|
}
|
||||||
})
|
}
|
||||||
}, [topic.id])
|
}, [assistant, autoRenameTopicMemo, dispatch, messages, handleDeleteMessage, scrollToBottom, topic, updateTopic])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
@ -264,21 +188,10 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
})
|
})
|
||||||
}, [assistant, messages])
|
}, [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(() => {
|
const loadMoreMessages = useCallback(() => {
|
||||||
if (!hasMore || isLoadingMore) return
|
if (!hasMore || isLoadingMore) return
|
||||||
|
|
||||||
setIsLoadingMore(true)
|
setIsLoadingMore(true)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentLength = displayMessages.length
|
const currentLength = displayMessages.length
|
||||||
const reversedMessages = [...messages].reverse()
|
const reversedMessages = [...messages].reverse()
|
||||||
@ -288,7 +201,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
|
||||||
setIsLoadingMore(false)
|
setIsLoadingMore(false)
|
||||||
}, 300)
|
}, 300)
|
||||||
}, [displayMessages, hasMore, isLoadingMore, messages])
|
}, [displayMessages.length, hasMore, isLoadingMore, messages])
|
||||||
|
|
||||||
useShortcut('copy_last_message', () => {
|
useShortcut('copy_last_message', () => {
|
||||||
const lastMessage = last(messages)
|
const lastMessage = last(messages)
|
||||||
@ -315,19 +228,19 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
inverse={true}
|
inverse={true}
|
||||||
scrollableTarget="messages">
|
scrollableTarget="messages">
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
<LoaderContainer $loading={isLoadingMore}>
|
<LoaderContainer $loading={loading || isLoadingMore}>
|
||||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||||
</LoaderContainer>
|
</LoaderContainer>
|
||||||
{Object.entries(groupedMessages).map(([key, messages]) => (
|
{Object.entries(getGroupedMessages(displayMessages)).map(([key, groupMessages]) => (
|
||||||
<MessageGroup
|
<MessageGroup
|
||||||
key={key}
|
key={key}
|
||||||
messages={messages}
|
messages={groupMessages}
|
||||||
topic={topic}
|
topic={topic}
|
||||||
hidePresetMessages={assistant.settings?.hideMessages}
|
hidePresetMessages={assistant.settings?.hideMessages}
|
||||||
onSetMessages={setMessages}
|
onSetMessages={setDisplayMessages}
|
||||||
onDeleteMessage={onDeleteMessage}
|
onDeleteMessage={handleDeleteMessage}
|
||||||
onDeleteGroupMessages={onDeleteGroupMessages}
|
onDeleteGroupMessages={handleDeleteGroupMessages}
|
||||||
onGetMessages={onGetMessages}
|
onGetMessages={() => messages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { fetchSuggestions } from '@renderer/services/ApiService'
|
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 { Assistant, Message, Suggestion } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { last } from 'lodash'
|
import { last } from 'lodash'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, memo, useEffect, useState } from 'react'
|
||||||
import BeatLoader from 'react-spinners/BeatLoader'
|
import BeatLoader from 'react-spinners/BeatLoader'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
@ -16,40 +14,46 @@ interface Props {
|
|||||||
const suggestionsMap = new Map<string, Suggestion[]>()
|
const suggestionsMap = new Map<string, Suggestion[]>()
|
||||||
|
|
||||||
const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const [suggestions, setSuggestions] = useState<Suggestion[]>(
|
const [suggestions, setSuggestions] = useState<Suggestion[]>(
|
||||||
suggestionsMap.get(messages[messages.length - 1]?.id) || []
|
suggestionsMap.get(messages[messages.length - 1]?.id) || []
|
||||||
)
|
)
|
||||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
|
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
|
||||||
|
|
||||||
const onClick = (s: Suggestion) => {
|
const handleSuggestionClick = async (content: string) => {
|
||||||
const message: Message = {
|
await dispatch(sendMessage(content, assistant, assistant.topics[0]))
|
||||||
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'
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
suggestionsHandle()
|
||||||
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
// const unsubscribes = [
|
||||||
setLoadingSuggestions(true)
|
// EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
||||||
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
|
|
||||||
if (_suggestions.length) {
|
// ]
|
||||||
setSuggestions(_suggestions)
|
// return () => {
|
||||||
suggestionsMap.set(msg.id, _suggestions)
|
// for (const unsub of unsubscribes) {
|
||||||
}
|
// unsub()
|
||||||
setLoadingSuggestions(false)
|
// }
|
||||||
})
|
// }
|
||||||
]
|
}, []) // Remove messages dependency
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
|
||||||
}, [assistant, messages])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
|
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
|
||||||
@ -58,7 +62,6 @@ const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
|||||||
if (last(messages)?.status !== 'success') {
|
if (last(messages)?.status !== 'success') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingSuggestions) {
|
if (loadingSuggestions) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@ -75,7 +78,7 @@ const Suggestions: FC<Props> = ({ assistant, messages }) => {
|
|||||||
<Container>
|
<Container>
|
||||||
<SuggestionsContainer>
|
<SuggestionsContainer>
|
||||||
{suggestions.map((s, i) => (
|
{suggestions.map((s, i) => (
|
||||||
<SuggestionItem key={i} onClick={() => onClick(s)}>
|
<SuggestionItem key={i} onClick={() => handleSuggestionClick(s.content)}>
|
||||||
{s.content} →
|
{s.content} →
|
||||||
</SuggestionItem>
|
</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) {
|
} catch (error: any) {
|
||||||
|
console.log('error', error)
|
||||||
message.status = 'error'
|
message.status = 'error'
|
||||||
message.error = formatMessageError(error)
|
message.error = formatMessageError(error)
|
||||||
}
|
}
|
||||||
@ -216,7 +217,6 @@ export async function fetchSuggestions({
|
|||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
}): Promise<Suggestion[]> {
|
}): Promise<Suggestion[]> {
|
||||||
const model = assistant.model
|
const model = assistant.model
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export const EventEmitter = new Emittery()
|
|||||||
|
|
||||||
export const EVENT_NAMES = {
|
export const EVENT_NAMES = {
|
||||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||||
APPEND_MESSAGE: 'APPEND_MESSAGE',
|
// APPEND_MESSAGE: 'APPEND_MESSAGE',
|
||||||
RECEIVE_MESSAGE: 'RECEIVE_MESSAGE',
|
RECEIVE_MESSAGE: 'RECEIVE_MESSAGE',
|
||||||
AI_AUTO_RENAME: 'AI_AUTO_RENAME',
|
AI_AUTO_RENAME: 'AI_AUTO_RENAME',
|
||||||
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import assistants from './assistants'
|
|||||||
import knowledge from './knowledge'
|
import knowledge from './knowledge'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
import mcp from './mcp'
|
import mcp from './mcp'
|
||||||
|
import messagesReducer from './messages'
|
||||||
import migrate from './migrate'
|
import migrate from './migrate'
|
||||||
import minapps from './minapps'
|
import minapps from './minapps'
|
||||||
import paintings from './paintings'
|
import paintings from './paintings'
|
||||||
@ -27,6 +28,7 @@ const rootReducer = combineReducers({
|
|||||||
knowledge,
|
knowledge,
|
||||||
minapps,
|
minapps,
|
||||||
websearch,
|
websearch,
|
||||||
|
messages: messagesReducer,
|
||||||
mcp
|
mcp
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ const persistedReducer = persistReducer(
|
|||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 77,
|
version: 77,
|
||||||
blacklist: ['runtime'],
|
blacklist: ['runtime', 'messages'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
rootReducer
|
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
|
linkType: hard
|
||||||
|
|
||||||
"@types/ws@npm:^8.5.4":
|
"@types/ws@npm:^8.5.4":
|
||||||
version: 8.5.14
|
version: 8.18.0
|
||||||
resolution: "@types/ws@npm:8.5.14"
|
resolution: "@types/ws@npm:8.18.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node": "npm:*"
|
"@types/node": "npm:*"
|
||||||
checksum: 10c0/be88a0b6252f939cb83340bd1b4d450287f752c19271195cd97564fd94047259a9bb8c31c585a61b69d8a1b069a99df9dd804db0132d3359c54d3890c501416a
|
checksum: 10c0/a56d2e0d1da7411a1f3548ce02b51a50cbe9e23f025677d03df48f87e4a3c72e1342fbf1d12e487d7eafa8dc670c605152b61bbf9165891ec0e9694b0d3ea8d4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -3184,6 +3184,7 @@ __metadata:
|
|||||||
mime: "npm:^4.0.4"
|
mime: "npm:^4.0.4"
|
||||||
officeparser: "npm:^4.1.1"
|
officeparser: "npm:^4.1.1"
|
||||||
openai: "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
|
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"
|
prettier: "npm:^3.2.4"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
react-dom: "npm:^18.2.0"
|
react-dom: "npm:^18.2.0"
|
||||||
@ -3262,11 +3263,11 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"acorn@npm:^8.9.0":
|
"acorn@npm:^8.9.0":
|
||||||
version: 8.14.0
|
version: 8.14.1
|
||||||
resolution: "acorn@npm:8.14.0"
|
resolution: "acorn@npm:8.14.1"
|
||||||
bin:
|
bin:
|
||||||
acorn: bin/acorn
|
acorn: bin/acorn
|
||||||
checksum: 10c0/6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7
|
checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -3420,8 +3421,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"antd@npm:^5.22.5":
|
"antd@npm:^5.22.5":
|
||||||
version: 5.24.2
|
version: 5.24.3
|
||||||
resolution: "antd@npm:5.24.2"
|
resolution: "antd@npm:5.24.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ant-design/colors": "npm:^7.2.0"
|
"@ant-design/colors": "npm:^7.2.0"
|
||||||
"@ant-design/cssinjs": "npm:^1.23.0"
|
"@ant-design/cssinjs": "npm:^1.23.0"
|
||||||
@ -3438,7 +3439,7 @@ __metadata:
|
|||||||
classnames: "npm:^2.5.1"
|
classnames: "npm:^2.5.1"
|
||||||
copy-to-clipboard: "npm:^3.3.3"
|
copy-to-clipboard: "npm:^3.3.3"
|
||||||
dayjs: "npm:^1.11.11"
|
dayjs: "npm:^1.11.11"
|
||||||
rc-cascader: "npm:~3.33.0"
|
rc-cascader: "npm:~3.33.1"
|
||||||
rc-checkbox: "npm:~3.5.0"
|
rc-checkbox: "npm:~3.5.0"
|
||||||
rc-collapse: "npm:~3.9.0"
|
rc-collapse: "npm:~3.9.0"
|
||||||
rc-dialog: "npm:~9.6.0"
|
rc-dialog: "npm:~9.6.0"
|
||||||
@ -3446,14 +3447,14 @@ __metadata:
|
|||||||
rc-dropdown: "npm:~4.2.1"
|
rc-dropdown: "npm:~4.2.1"
|
||||||
rc-field-form: "npm:~2.7.0"
|
rc-field-form: "npm:~2.7.0"
|
||||||
rc-image: "npm:~7.11.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-input-number: "npm:~9.4.0"
|
||||||
rc-mentions: "npm:~2.19.1"
|
rc-mentions: "npm:~2.19.1"
|
||||||
rc-menu: "npm:~9.16.1"
|
rc-menu: "npm:~9.16.1"
|
||||||
rc-motion: "npm:^2.9.5"
|
rc-motion: "npm:^2.9.5"
|
||||||
rc-notification: "npm:~5.6.3"
|
rc-notification: "npm:~5.6.3"
|
||||||
rc-pagination: "npm:~5.1.0"
|
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-progress: "npm:~4.0.0"
|
||||||
rc-rate: "npm:~2.13.1"
|
rc-rate: "npm:~2.13.1"
|
||||||
rc-resize-observer: "npm:^1.4.3"
|
rc-resize-observer: "npm:^1.4.3"
|
||||||
@ -3466,7 +3467,7 @@ __metadata:
|
|||||||
rc-tabs: "npm:~15.5.1"
|
rc-tabs: "npm:~15.5.1"
|
||||||
rc-textarea: "npm:~1.9.0"
|
rc-textarea: "npm:~1.9.0"
|
||||||
rc-tooltip: "npm:~6.4.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-tree-select: "npm:~5.27.0"
|
||||||
rc-upload: "npm:~4.8.1"
|
rc-upload: "npm:~4.8.1"
|
||||||
rc-util: "npm:^5.44.4"
|
rc-util: "npm:^5.44.4"
|
||||||
@ -3475,7 +3476,7 @@ __metadata:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.9.0"
|
react: ">=16.9.0"
|
||||||
react-dom: ">=16.9.0"
|
react-dom: ">=16.9.0"
|
||||||
checksum: 10c0/a6772136b828ef73925af633dee66b7580bad736eac20c713d1991e457aae3879d1a826ca06cc40f824238da0ad1a9745dbde6acbd801aec589deaeef04846fe
|
checksum: 10c0/bbd1dcc4cce3bebecebde135014352156d57c8979b1ad8cb8cf873e8919454292a812f6d2e8995673b82f3cb46a2cdd14ea7b2190193bd5127380c1091ce472a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -4926,15 +4927,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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
|
version: 4.4.0
|
||||||
resolution: "debug@npm:4.4.0"
|
resolution: "debug@npm:4.4.0"
|
||||||
@ -4959,6 +4951,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"debug@npm:^3.1.0, debug@npm:^3.2.6":
|
||||||
version: 3.2.7
|
version: 3.2.7
|
||||||
resolution: "debug@npm:3.2.7"
|
resolution: "debug@npm:3.2.7"
|
||||||
@ -5533,9 +5534,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron-log@npm:^5.1.5":
|
"electron-log@npm:^5.1.5":
|
||||||
version: 5.3.1
|
version: 5.3.2
|
||||||
resolution: "electron-log@npm:5.3.1"
|
resolution: "electron-log@npm:5.3.2"
|
||||||
checksum: 10c0/051157400b36f4ad51c52ae30a6c37d0c1525dc582312dc4043723d0e52aa11a95c28ee1421a0067f11ad641624e1163850e24d1c8cec47ba31aaf905953100f
|
checksum: 10c0/8cfcd6eb6ab2dff010941f9a39793a411646dc0462991632d2427fc9a45f22b00ec758996629f8c203612d99cb01713c08c72d2b0454603d13386f433f5b755b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -5565,9 +5566,9 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"electron-to-chromium@npm:^1.5.73":
|
"electron-to-chromium@npm:^1.5.73":
|
||||||
version: 1.5.112
|
version: 1.5.113
|
||||||
resolution: "electron-to-chromium@npm:1.5.112"
|
resolution: "electron-to-chromium@npm:1.5.113"
|
||||||
checksum: 10c0/fc597268d6d3d7458b55141c436802a6c51078855f021823cdb380b80ad1a69e1c2899fdfc9cffa501d47feb3791ea6a75893fe802a608c7845e979a48f5ac25
|
checksum: 10c0/837fe2fd26adbc4f3ad8e758d14067a14f636f9c2923b5ded8adb93426bbe3fdc83b48ddf9f2cf03be31b5becb0c31144db19c823b696fd52a7bc4583f4bde00
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -5681,13 +5682,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"encoding@npm:^0.1.12, encoding@npm:^0.1.13":
|
||||||
version: 0.1.13
|
version: 0.1.13
|
||||||
resolution: "encoding@npm:0.1.13"
|
resolution: "encoding@npm:0.1.13"
|
||||||
@ -6260,6 +6254,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"events@npm:^3.3.0":
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
resolution: "events@npm:3.3.0"
|
resolution: "events@npm:3.3.0"
|
||||||
@ -6602,17 +6603,16 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"finalhandler@npm:^2.0.0":
|
"finalhandler@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.1.0
|
||||||
resolution: "finalhandler@npm:2.0.0"
|
resolution: "finalhandler@npm:2.1.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: "npm:2.6.9"
|
debug: "npm:^4.4.0"
|
||||||
encodeurl: "npm:~1.0.2"
|
encodeurl: "npm:^2.0.0"
|
||||||
escape-html: "npm:~1.0.3"
|
escape-html: "npm:^1.0.3"
|
||||||
on-finished: "npm:2.4.1"
|
on-finished: "npm:^2.4.1"
|
||||||
parseurl: "npm:~1.3.3"
|
parseurl: "npm:^1.3.3"
|
||||||
statuses: "npm:2.0.1"
|
statuses: "npm:^2.0.1"
|
||||||
unpipe: "npm:~1.0.0"
|
checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f
|
||||||
checksum: 10c0/ca6f69d69797eebc900d7627bde4bb7d38417112911eb11ce4e40011195b6ad1a09413ad082da9bb64da789a4ecfffdd0e6a5ea1ccb4147062224c3050f134ea
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -7478,8 +7478,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"hast-util-to-jsx-runtime@npm:^2.0.0":
|
"hast-util-to-jsx-runtime@npm:^2.0.0":
|
||||||
version: 2.3.5
|
version: 2.3.6
|
||||||
resolution: "hast-util-to-jsx-runtime@npm:2.3.5"
|
resolution: "hast-util-to-jsx-runtime@npm:2.3.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree": "npm:^1.0.0"
|
"@types/estree": "npm:^1.0.0"
|
||||||
"@types/hast": "npm:^3.0.0"
|
"@types/hast": "npm:^3.0.0"
|
||||||
@ -7493,10 +7493,10 @@ __metadata:
|
|||||||
mdast-util-mdxjs-esm: "npm:^2.0.0"
|
mdast-util-mdxjs-esm: "npm:^2.0.0"
|
||||||
property-information: "npm:^7.0.0"
|
property-information: "npm:^7.0.0"
|
||||||
space-separated-tokens: "npm:^2.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"
|
unist-util-position: "npm:^5.0.0"
|
||||||
vfile-message: "npm:^4.0.0"
|
vfile-message: "npm:^4.0.0"
|
||||||
checksum: 10c0/9db65b2b417cdaad1f1cc619b613abd8d1fa7196f5979ce54bd1dc8a937613f11fecb8b7a43425342cf36fd085b0fed89daadcce43bed8762786a4cdc21a1df8
|
checksum: 10c0/27297e02848fe37ef219be04a26ce708d17278a175a807689e94a821dcffc88aa506d62c3a85beed1f9a8544f7211bdcbcde0528b7b456a57c2e342c3fd11056
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -10979,6 +10979,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"p-retry@npm:4":
|
||||||
version: 4.6.2
|
version: 4.6.2
|
||||||
resolution: "p-retry@npm:4.6.2"
|
resolution: "p-retry@npm:4.6.2"
|
||||||
@ -11005,6 +11015,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"p-try@npm:^2.0.0":
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
resolution: "p-try@npm:2.2.0"
|
resolution: "p-try@npm:2.2.0"
|
||||||
@ -11691,7 +11708,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rc-cascader@npm:~3.33.0":
|
"rc-cascader@npm:~3.33.1":
|
||||||
version: 3.33.1
|
version: 3.33.1
|
||||||
resolution: "rc-cascader@npm:3.33.1"
|
resolution: "rc-cascader@npm:3.33.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11830,7 +11847,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.7.3
|
||||||
resolution: "rc-input@npm:1.7.3"
|
resolution: "rc-input@npm:1.7.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11937,7 +11954,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rc-picker@npm:~4.11.2":
|
"rc-picker@npm:~4.11.3":
|
||||||
version: 4.11.3
|
version: 4.11.3
|
||||||
resolution: "rc-picker@npm:4.11.3"
|
resolution: "rc-picker@npm:4.11.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -12086,8 +12103,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rc-table@npm:~7.50.3":
|
"rc-table@npm:~7.50.3":
|
||||||
version: 7.50.3
|
version: 7.50.4
|
||||||
resolution: "rc-table@npm:7.50.3"
|
resolution: "rc-table@npm:7.50.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime": "npm:^7.10.1"
|
"@babel/runtime": "npm:^7.10.1"
|
||||||
"@rc-component/context": "npm:^1.4.0"
|
"@rc-component/context": "npm:^1.4.0"
|
||||||
@ -12098,7 +12115,7 @@ __metadata:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.9.0"
|
react: ">=16.9.0"
|
||||||
react-dom: ">=16.9.0"
|
react-dom: ">=16.9.0"
|
||||||
checksum: 10c0/61fee18289063d33e135f87e7d325c4ab319db6a452bc7151b13c9bfdcedc9a280a543fe06dea05cd41814c47dfc0c5b3ade876f7e6c286714a8276a53f7125b
|
checksum: 10c0/ab5eb3db00bc31470d7dd5946c1a919247742703a3278ea2d9a33e719a08e170ca80977e615084c7cfcb54508a9c3da2449409cce16fc1d7ec1ea67596c30d79
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -12167,7 +12184,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rc-tree@npm:~5.13.0":
|
"rc-tree@npm:~5.13.0, rc-tree@npm:~5.13.1":
|
||||||
version: 5.13.1
|
version: 5.13.1
|
||||||
resolution: "rc-tree@npm:5.13.1"
|
resolution: "rc-tree@npm:5.13.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -12211,8 +12228,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"rc-virtual-list@npm:^3.14.2, rc-virtual-list@npm:^3.5.1, rc-virtual-list@npm:^3.5.2":
|
"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
|
version: 3.18.4
|
||||||
resolution: "rc-virtual-list@npm:3.18.3"
|
resolution: "rc-virtual-list@npm:3.18.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime": "npm:^7.20.0"
|
"@babel/runtime": "npm:^7.20.0"
|
||||||
classnames: "npm:^2.2.6"
|
classnames: "npm:^2.2.6"
|
||||||
@ -12221,7 +12238,7 @@ __metadata:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.9.0"
|
react: ">=16.9.0"
|
||||||
react-dom: ">=16.9.0"
|
react-dom: ">=16.9.0"
|
||||||
checksum: 10c0/773b18d9594d78b4f08182caec3043fef6c1b1871db91e05294b62a65e309e2d0ee50ca699a1b237af1ac0b1b1c9cc25d62f47bae30d4a0c0dcab0ddc4d5bb8d
|
checksum: 10c0/2566e98418b8072af4591488e028a1e8433d2a190716f47134ee63a506afec9cf472f8a8d3dda55bf2e82ccf1215d324abd280892db4fbf5999fc30184068f62
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -13961,7 +13978,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.0.8
|
||||||
resolution: "style-to-object@npm:1.0.8"
|
resolution: "style-to-object@npm:1.0.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -14737,7 +14763,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
"unpipe@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "unpipe@npm:1.0.0"
|
resolution: "unpipe@npm:1.0.0"
|
||||||
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
|
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user