mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 01:30:51 +08:00
* 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>
254 lines
7.5 KiB
TypeScript
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)
|