cherry-studio/src/renderer/src/pages/home/Messages/MessageGroup.tsx
MyPrototypeWhat 2fe21c7d41 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>
2025-03-08 01:41:05 +08:00

254 lines
7.5 KiB
TypeScript

import Scrollbar from '@renderer/components/Scrollbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Message, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Popover } from 'antd'
import type { Dispatch, SetStateAction } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import MessageGroupMenuBar from './MessageGroupMenuBar'
import MessageStream from './MessageStream'
interface Props {
messages: (Message & { index: number })[]
topic: Topic
hidePresetMessages?: boolean
onGetMessages: () => Message[]
onSetMessages: Dispatch<SetStateAction<Message[]>>
onDeleteMessage: (message: Message) => Promise<void>
onDeleteGroupMessages: (askId: string) => Promise<void>
}
const MessageGroup = ({
messages,
topic,
hidePresetMessages,
onDeleteMessage,
onSetMessages,
onGetMessages,
onDeleteGroupMessages
}: Props) => {
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const { t } = useTranslation()
const [multiModelMessageStyle, setMultiModelMessageStyle] =
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
const isGrouped = messageLength > 1
const isHorizontal = multiModelMessageStyle === 'horizontal'
const isGrid = multiModelMessageStyle === 'grid'
const handleDeleteGroup = useCallback(async () => {
const askId = messages[0]?.askId
if (!askId) return
window.modal.confirm({
title: t('message.group.delete.title'),
content: t('message.group.delete.content'),
centered: true,
okButtonProps: {
danger: true
},
okText: t('common.delete'),
onOk: () => onDeleteGroupMessages(askId)
})
}, [messages, onDeleteGroupMessages, t])
useEffect(() => {
setSelectedIndex(messageLength - 1)
}, [messageLength])
const renderMessage = useCallback(
(message: Message & { index: number }, index: number) => {
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
const messageProps = {
isGrouped,
message,
topic,
index: message.index,
hidePresetMessages,
style: {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
},
onSetMessages,
onDeleteMessage,
onGetMessages
}
const messageWrapper = (
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
<MessageStream {...messageProps} />
</MessageWrapper>
)
if (isGridGroupMessage) {
return (
<Popover
key={message.id}
content={
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
$isInPopover={true}>
<MessageStream {...messageProps} />
</MessageWrapper>
}
trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}>
{messageWrapper}
</Popover>
)
}
return messageWrapper
},
[
isGrid,
isGrouped,
isHorizontal,
multiModelMessageStyle,
selectedIndex,
topic,
hidePresetMessages,
onSetMessages,
onDeleteMessage,
onGetMessages,
gridPopoverTrigger
]
)
return (
<GroupContainer
$isGrouped={isGrouped}
$layout={multiModelMessageStyle}
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
<GridContainer
$count={messageLength}
$layout={multiModelMessageStyle}
$gridColumns={gridColumns}
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
{messages.map((message, index) => renderMessage(message, index))}
</GridContainer>
{isGrouped && (
<MessageGroupMenuBar
multiModelMessageStyle={multiModelMessageStyle}
setMultiModelMessageStyle={setMultiModelMessageStyle}
messages={messages}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
onDelete={handleDeleteGroup}
/>
)}
</GroupContainer>
)
}
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
&.group-container.horizontal,
&.group-container.grid {
padding: 0 20px;
.message {
padding: 0;
}
.group-menu-bar {
margin-left: 0;
margin-right: 0;
}
}
`
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
width: 100%;
display: grid;
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
overflow-y: auto;
grid-template-columns: repeat(
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(550px, 1fr)
);
@media (max-width: 800px) {
grid-template-columns: repeat(
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(400px, 1fr)
);
}
${({ $layout }) =>
$layout === 'horizontal' &&
css`
margin-top: 15px;
`}
${({ $gridColumns, $layout, $count }) =>
$layout === 'grid' &&
css`
margin-top: 15px;
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
grid-template-rows: auto;
gap: 16px;
`}
`
interface MessageWrapperProps {
$layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
$selected: boolean
$isGrouped: boolean
$isInPopover?: boolean
}
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%;
display: ${(props) => {
if (props.$layout === 'fold') {
return props.$selected ? 'block' : 'none'
}
if (props.$layout === 'horizontal') {
return 'inline-block'
}
return 'block'
}};
${({ $layout, $isGrouped }) => {
if ($layout === 'horizontal' && $isGrouped) {
return css`
border: 0.5px solid var(--color-border);
padding: 10px;
border-radius: 6px;
max-height: 600px;
margin-bottom: 10px;
`
}
return ''
}}
${({ $layout, $isInPopover, $isGrouped }) => {
return $layout === 'grid' && $isGrouped
? css`
max-height: ${$isInPopover ? '50vh' : '300px'};
overflow-y: ${$isInPopover ? 'auto' : 'hidden'};
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
padding: 10px;
border-radius: 6px;
background-color: var(--color-background);
`
: css`
overflow-y: auto;
border-radius: 6px;
`
}}
`
export default memo(MessageGroup)