From 6973f4eebdf73a0f339f338266f39a4b6e3905f2 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sun, 24 Nov 2024 18:29:19 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20refactor=20the?= =?UTF-8?q?=20main=20chat=20(#4773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: refactor the main chat * ♻️ refactor: refactor welcome --- .../features/ChatList/ChatItem/index.tsx | 39 ++ .../features/ChatList/Content.tsx | 34 +- .../InboxWelcome/AgentsSuggest.tsx | 0 .../InboxWelcome/QuestionSuggest.tsx | 0 .../WelcomeChatItem}/InboxWelcome/index.tsx | 0 .../WelcomeChatItem/WelcomeMessage.tsx | 44 ++ .../ChatList/WelcomeChatItem/index.tsx | 17 + .../(workspace)/@portal/features/Header.tsx | 14 +- src/components/BrandWatermark/index.tsx | 1 + src/const/message.ts | 2 +- .../components/ChatItem/ActionsBar.tsx | 22 +- .../components/ChatItem/index.tsx | 392 ++++++++---------- .../components/VirtualizedList/index.tsx | 159 ++++--- src/features/Conversation/index.ts | 1 - src/features/Portal/Artifacts/index.ts | 2 +- src/features/Portal/FilePreview/index.ts | 2 +- .../Portal/Home/{Header.tsx => Title.tsx} | 4 +- src/features/Portal/Home/index.ts | 2 +- src/features/Portal/MessageDetail/index.ts | 2 +- src/features/Portal/Plugins/index.ts | 2 +- src/features/Portal/components/Header.tsx | 29 ++ src/features/Portal/router.tsx | 25 +- src/features/Portal/type.ts | 4 +- .../ShareImage}/ChatList/index.tsx | 7 +- .../ShareModal/ShareImage/Preview.tsx | 2 +- src/features/ShareModal/ShareJSON/index.tsx | 2 +- src/features/ShareModal/ShareText/index.tsx | 2 +- src/layout/GlobalProvider/Debug.tsx | 15 + src/layout/GlobalProvider/index.tsx | 2 + .../slices/aiChat/actions/generateAIChat.ts | 10 +- src/store/chat/slices/message/action.ts | 4 +- .../chat/slices/message/selectors.test.ts | 86 ---- src/store/chat/slices/message/selectors.ts | 128 +++--- src/store/chat/slices/plugin/action.test.ts | 4 +- src/store/chat/slices/plugin/action.ts | 2 +- src/store/chat/slices/topic/action.ts | 4 +- 36 files changed, 541 insertions(+), 524 deletions(-) create mode 100644 src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx rename src/{features/Conversation/components => app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem}/InboxWelcome/AgentsSuggest.tsx (100%) rename src/{features/Conversation/components => app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem}/InboxWelcome/QuestionSuggest.tsx (100%) rename src/{features/Conversation/components => app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem}/InboxWelcome/index.tsx (100%) create mode 100644 src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx create mode 100644 src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/index.tsx rename src/features/Portal/Home/{Header.tsx => Title.tsx} (85%) create mode 100644 src/features/Portal/components/Header.tsx rename src/features/{Conversation/components => ShareModal/ShareImage}/ChatList/index.tsx (70%) create mode 100644 src/layout/GlobalProvider/Debug.tsx diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx new file mode 100644 index 000000000000..f3ab06b5458f --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx @@ -0,0 +1,39 @@ +import React, { memo, useMemo } from 'react'; + +import { ChatItem } from '@/features/Conversation'; +import ActionsBar from '@/features/Conversation/components/ChatItem/ActionsBar'; +import { useAgentStore } from '@/store/agent'; +import { agentSelectors } from '@/store/agent/selectors'; +import { useChatStore } from '@/store/chat'; +import { chatSelectors } from '@/store/chat/selectors'; + +export interface ThreadChatItemProps { + id: string; + index: number; +} + +const MainChatItem = memo(({ id, index }) => { + const [historyLength] = useChatStore((s) => [chatSelectors.mainDisplayChatIDs(s).length]); + + const enableHistoryDivider = useAgentStore((s) => { + const config = agentSelectors.currentAgentChatConfig(s); + return ( + config.enableHistoryCount && + historyLength > (config.historyCount ?? 0) && + config.historyCount === historyLength - index + ); + }); + + const actionBar = useMemo(() => , [id]); + + return ( + + ); +}); + +export default MainChatItem; diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx index 6c491515a8f3..35e3507a747f 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx @@ -1,35 +1,41 @@ 'use client'; -import isEqual from 'fast-deep-equal'; -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; -import { InboxWelcome, VirtualizedList } from '@/features/Conversation'; +import { SkeletonList, VirtualizedList } from '@/features/Conversation'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; import { useSessionStore } from '@/store/session'; +import MainChatItem from './ChatItem'; +import Welcome from './WelcomeChatItem'; + interface ListProps { mobile?: boolean; } const Content = memo(({ mobile }) => { - const [activeTopicId, useFetchMessages, showInboxWelcome, isCurrentChatLoaded] = useChatStore( - (s) => [ - s.activeTopicId, - s.useFetchMessages, - chatSelectors.showInboxWelcome(s), - chatSelectors.isCurrentChatLoaded(s), - ], - ); + const [activeTopicId, useFetchMessages, isCurrentChatLoaded] = useChatStore((s) => [ + s.activeTopicId, + s.useFetchMessages, + chatSelectors.isCurrentChatLoaded(s), + ]); const [sessionId] = useSessionStore((s) => [s.activeId]); useFetchMessages(sessionId, activeTopicId); - const data = useChatStore(chatSelectors.currentChatIDsWithGuideMessage, isEqual); + const data = useChatStore(chatSelectors.mainDisplayChatIDs); + + const itemContent = useCallback( + (index: number, id: string) => , + [mobile], + ); + + if (!isCurrentChatLoaded) return ; - if (showInboxWelcome && isCurrentChatLoaded) return ; + if (data.length === 0) return ; - return ; + return ; }); Content.displayName = 'ChatListRender'; diff --git a/src/features/Conversation/components/InboxWelcome/AgentsSuggest.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx similarity index 100% rename from src/features/Conversation/components/InboxWelcome/AgentsSuggest.tsx rename to src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx diff --git a/src/features/Conversation/components/InboxWelcome/QuestionSuggest.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx similarity index 100% rename from src/features/Conversation/components/InboxWelcome/QuestionSuggest.tsx rename to src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx diff --git a/src/features/Conversation/components/InboxWelcome/index.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/index.tsx similarity index 100% rename from src/features/Conversation/components/InboxWelcome/index.tsx rename to src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/index.tsx diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx new file mode 100644 index 000000000000..9db1df92bd00 --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx @@ -0,0 +1,44 @@ +import { ChatItem } from '@lobehub/ui'; +import isEqual from 'fast-deep-equal'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAgentStore } from '@/store/agent'; +import { agentSelectors } from '@/store/agent/selectors'; +import { useChatStore } from '@/store/chat'; +import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig'; +import { useSessionStore } from '@/store/session'; +import { sessionMetaSelectors } from '@/store/session/selectors'; + +const WelcomeMessage = () => { + const { t } = useTranslation('chat'); + const [type = 'chat'] = useAgentStore((s) => { + const config = agentSelectors.currentAgentChatConfig(s); + return [config.displayMode]; + }); + + const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); + const { isAgentEditable } = useServerConfigStore(featureFlagsSelectors); + const activeId = useChatStore((s) => s.activeId); + + const agentSystemRoleMsg = t('agentDefaultMessageWithSystemRole', { + name: meta.title || t('defaultAgent'), + systemRole: meta.description, + }); + + const agentMsg = t(isAgentEditable ? 'agentDefaultMessage' : 'agentDefaultMessageWithoutEdit', { + name: meta.title || t('defaultAgent'), + url: `/chat/settings?session=${activeId}`, + }); + + return ( + + ); +}; +export default WelcomeMessage; diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/index.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/index.tsx new file mode 100644 index 000000000000..7b09ec286749 --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/index.tsx @@ -0,0 +1,17 @@ +import React, { memo } from 'react'; + +import { useChatStore } from '@/store/chat'; +import { chatSelectors } from '@/store/chat/selectors'; + +import InboxWelcome from './InboxWelcome'; +import WelcomeMessage from './WelcomeMessage'; + +const WelcomeChatItem = memo(() => { + const showInboxWelcome = useChatStore(chatSelectors.showInboxWelcome); + + if (showInboxWelcome) return ; + + return ; +}); + +export default WelcomeChatItem; diff --git a/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx b/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx index b3c96b7255c1..eda105d49c04 100644 --- a/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +++ b/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx @@ -1,23 +1,11 @@ 'use client'; -import { ActionIcon } from '@lobehub/ui'; -import { XIcon } from 'lucide-react'; import { memo } from 'react'; -import SidebarHeader from '@/components/SidebarHeader'; import { PortalHeader } from '@/features/Portal/router'; -import { useChatStore } from '@/store/chat'; const Header = memo(() => { - const [toggleInspector] = useChatStore((s) => [s.togglePortal]); - - return ( - toggleInspector(false)} />} - style={{ paddingBlock: 8, paddingInline: 8 }} - title={} - /> - ); + return ; }); export default Header; diff --git a/src/components/BrandWatermark/index.tsx b/src/components/BrandWatermark/index.tsx index d650c354e851..5cc38686f3eb 100644 --- a/src/components/BrandWatermark/index.tsx +++ b/src/components/BrandWatermark/index.tsx @@ -26,6 +26,7 @@ const BrandWatermark = memo>(({ style, ...rest }) return ( ((props) => { }); interface ActionsProps { - index: number; - setEditing: (edit: boolean) => void; + id: string; } -const Actions = memo(({ index, setEditing }) => { - const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); - const item = useChatStore( - (s) => chatSelectors.currentChatsWithGuideMessage(meta)(s)[index], - isEqual, - ); +const Actions = memo(({ id }) => { + const item = useChatStore(chatSelectors.getMessageById(id), isEqual); + const [toggleMessageEditing] = useChatStore((s) => [s.toggleMessageEditing]); const onActionsClick = useActionsClick(); const handleActionClick = useCallback( async (action: ActionEvent) => { switch (action.key) { case 'edit': { - setEditing(true); + toggleMessageEditing(id, true); } } + if (!item) return; onActionsClick(action, item); }, [item], ); - const RenderFunction = renderActions[item?.role] ?? ActionsBar; + const RenderFunction = renderActions[(item?.role || '') as MessageRoleType] ?? ActionsBar; - return ; + return ; }); export default Actions; diff --git a/src/features/Conversation/components/ChatItem/index.tsx b/src/features/Conversation/components/ChatItem/index.tsx index 9f02f8ab11bb..56c6cadccd95 100644 --- a/src/features/Conversation/components/ChatItem/index.tsx +++ b/src/features/Conversation/components/ChatItem/index.tsx @@ -5,13 +5,12 @@ import { createStyles } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { MouseEventHandler, ReactNode, memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; -import { useSessionStore } from '@/store/session'; -import { sessionMetaSelectors } from '@/store/session/selectors'; import { useUserStore } from '@/store/user'; import { userGeneralSettingsSelectors } from '@/store/user/selectors'; import { ChatMessage } from '@/types/message'; @@ -26,7 +25,6 @@ import { } from '../../Messages'; import History from '../History'; import { markdownElements } from '../MarkdownElements'; -import ActionsBar from './ActionsBar'; import { processWithArtifact } from './utils'; const rehypePlugins = markdownElements.map((element) => element.rehypePlugin); @@ -36,6 +34,7 @@ const useStyles = createStyles(({ css, prefixCls }) => ({ opacity: 0.6; `, message: css` + position: relative; // prevent the textarea too long .${prefixCls}-input { max-height: 900px; @@ -44,218 +43,193 @@ const useStyles = createStyles(({ css, prefixCls }) => ({ })); export interface ChatListItemProps { - hideActionBar?: boolean; + actionBar?: ReactNode; + className?: string; + enableHistoryDivider?: boolean; + endRender?: ReactNode; id: string; index: number; - showThreadDivider?: boolean; } -const Item = memo(({ index, id, hideActionBar }) => { - const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize); - const { t } = useTranslation('common'); - const { styles, cx } = useStyles(); - const [type = 'chat'] = useAgentStore((s) => { - const config = agentSelectors.currentAgentChatConfig(s); - return [config.displayMode]; - }); - - const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); - const item = useChatStore((s) => { - const chats = chatSelectors.currentChatsWithGuideMessage(meta)(s); - - if (index >= chats.length) return; - - return chats.find((s) => s.id === id); - }, isEqual); - - const [ - isMessageLoading, - generating, - isInRAGFlow, - editing, - toggleMessageEditing, - updateMessageContent, - ] = useChatStore((s) => [ - chatSelectors.isMessageLoading(id)(s), - chatSelectors.isMessageGenerating(id)(s), - chatSelectors.isMessageInRAGFlow(id)(s), - chatSelectors.isMessageEditing(id)(s), - s.toggleMessageEditing, - s.modifyMessageContent, - ]); - - // when the message is in RAG flow or the AI generating, it should be in loading state - const isProcessing = isInRAGFlow || generating; - - const onAvatarsClick = useAvatarsClick(item?.role); - - const renderMessage = useCallback( - (editableContent: ReactNode) => { - if (!item?.role) return; - const RenderFunction = renderMessages[item.role] ?? renderMessages['default']; - - if (!RenderFunction) return; - - return ; - }, - [item], - ); - - const BelowMessage = useCallback( - ({ data }: { data: ChatMessage }) => { - if (!item?.role) return; - const RenderFunction = renderBelowMessages[item.role] ?? renderBelowMessages['default']; - - if (!RenderFunction) return; - - return ; - }, - [item?.role], - ); - - const MessageExtra = useCallback( - ({ data }: { data: ChatMessage }) => { - if (!item?.role) return; - let RenderFunction; - if (renderMessagesExtra?.[item.role]) RenderFunction = renderMessagesExtra[item.role]; - - if (!RenderFunction) return; - return ; - }, - [item?.role], - ); - - const markdownCustomRender = useCallback( - (dom: ReactNode, { text }: { text: string }) => { - if (!item?.role) return dom; - let RenderFunction; - - if (renderMessagesExtra?.[item.role]) RenderFunction = markdownCustomRenders[item.role]; - if (!RenderFunction) return dom; - - return ; - }, - [item?.role, type], - ); - - const error = useErrorContent(item?.error); - - const [historyLength] = useChatStore((s) => [chatSelectors.currentChats(s).length]); - - const enableHistoryDivider = useAgentStore((s) => { - const config = agentSelectors.currentAgentChatConfig(s); +const Item = memo( + ({ className, enableHistoryDivider, id, actionBar, endRender }) => { + const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize); + const { t } = useTranslation('common'); + const { styles, cx } = useStyles(); + const [type = 'chat'] = useAgentStore((s) => { + const config = agentSelectors.currentAgentChatConfig(s); + return [config.displayMode]; + }); + + const item = useChatStore(chatSelectors.getMessageById(id), isEqual); + + const [ + isMessageLoading, + generating, + isInRAGFlow, + editing, + toggleMessageEditing, + updateMessageContent, + ] = useChatStore((s) => [ + chatSelectors.isMessageLoading(id)(s), + chatSelectors.isMessageGenerating(id)(s), + chatSelectors.isMessageInRAGFlow(id)(s), + chatSelectors.isMessageEditing(id)(s), + s.toggleMessageEditing, + s.modifyMessageContent, + ]); + + // when the message is in RAG flow or the AI generating, it should be in loading state + const isProcessing = isInRAGFlow || generating; + + const onAvatarsClick = useAvatarsClick(item?.role); + + const renderMessage = useCallback( + (editableContent: ReactNode) => { + if (!item?.role) return; + const RenderFunction = renderMessages[item.role] ?? renderMessages['default']; + + if (!RenderFunction) return; + + return ; + }, + [item], + ); + + const BelowMessage = useCallback( + ({ data }: { data: ChatMessage }) => { + if (!item?.role) return; + const RenderFunction = renderBelowMessages[item.role] ?? renderBelowMessages['default']; + + if (!RenderFunction) return; + + return ; + }, + [item?.role], + ); + + const MessageExtra = useCallback( + ({ data }: { data: ChatMessage }) => { + if (!item?.role) return; + let RenderFunction; + if (renderMessagesExtra?.[item.role]) RenderFunction = renderMessagesExtra[item.role]; + + if (!RenderFunction) return; + return ; + }, + [item?.role], + ); + + const markdownCustomRender = useCallback( + (dom: ReactNode, { text }: { text: string }) => { + if (!item?.role) return dom; + let RenderFunction; + + if (renderMessagesExtra?.[item.role]) RenderFunction = markdownCustomRenders[item.role]; + if (!RenderFunction) return dom; + + return ; + }, + [item?.role, type], + ); + + const error = useErrorContent(item?.error); + + // remove line breaks in artifact tag to make the ast transform easier + const message = + !editing && item?.role === 'assistant' ? processWithArtifact(item?.content) : item?.content; + + // ======================= Performance Optimization ======================= // + // these useMemo/useCallback are all for the performance optimization + // maybe we can remove it in React 19 + // ======================================================================== // + + const components = useMemo( + () => + Object.fromEntries( + markdownElements.map((element) => { + const Component = element.Component; + + return [element.tag, (props: any) => ]; + }), + ), + [id], + ); + + const markdownProps = useMemo( + () => ({ + components, + customRender: markdownCustomRender, + rehypePlugins, + }), + [components, markdownCustomRender], + ); + + const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]); + + const onDoubleClick = useCallback>( + (e) => { + if (!item) return; + if (item.id === 'default' || item.error) return; + if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) { + toggleMessageEditing(id, true); + } + }, + [item], + ); + + const text = useMemo( + () => ({ + cancel: t('cancel'), + confirm: t('ok'), + edit: t('edit'), + }), + [t], + ); + + const onEditingChange = useCallback((edit: boolean) => { + toggleMessageEditing(id, edit); + }, []); + + const belowMessage = useMemo(() => item && , [item]); + const errorMessage = useMemo(() => item && , [item]); + const messageExtra = useMemo(() => item && , [item]); + return ( - config.enableHistoryCount && - historyLength > (config.historyCount ?? 0) && - config.historyCount === historyLength - index + item && ( + <> + {enableHistoryDivider && } + + + {endRender} + + + ) ); - }); - - // remove line breaks in artifact tag to make the ast transform easier - const message = - !editing && item?.role === 'assistant' ? processWithArtifact(item?.content) : item?.content; - - // ======================= Performance Optimization ======================= // - // these useMemo/useCallback are all for the performance optimization - // maybe we can remove it in React 19 - // ======================================================================== // - - const components = useMemo( - () => - Object.fromEntries( - markdownElements.map((element) => { - const Component = element.Component; - - return [element.tag, (props: any) => ]; - }), - ), - [id], - ); - - const markdownProps = useMemo( - () => ({ - components, - customRender: markdownCustomRender, - rehypePlugins, - }), - [components, markdownCustomRender], - ); - - const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]); - - const onDoubleClick = useCallback>( - (e) => { - if (!item) return; - if (item.id === 'default' || item.error) return; - if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) { - toggleMessageEditing(id, true); - } - }, - [item], - ); - - const text = useMemo( - () => ({ - cancel: t('cancel'), - confirm: t('ok'), - edit: t('edit'), - }), - [t], - ); - - const onEditingChange = useCallback((edit: boolean) => { - toggleMessageEditing(id, edit); - }, []); - - const actions = useMemo( - () => - !hideActionBar && ( - { - toggleMessageEditing(id, edit); - }} - /> - ), - [hideActionBar, index, id], - ); - - const belowMessage = useMemo(() => item && , [item]); - const errorMessage = useMemo(() => item && , [item]); - const messageExtra = useMemo(() => item && , [item]); - - return ( - item && ( - <> - {enableHistoryDivider && } - - - ) - ); -}); + }, +); Item.displayName = 'ChatItem'; diff --git a/src/features/Conversation/components/VirtualizedList/index.tsx b/src/features/Conversation/components/VirtualizedList/index.tsx index 495dc913313b..0adeb559490f 100644 --- a/src/features/Conversation/components/VirtualizedList/index.tsx +++ b/src/features/Conversation/components/VirtualizedList/index.tsx @@ -12,105 +12,96 @@ import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; import AutoScroll from '../AutoScroll'; -import Item from '../ChatItem'; import SkeletonList from '../SkeletonList'; interface VirtualizedListProps { dataSource: string[]; - hideActionBar?: boolean; - itemContent?: (index: number, data: any, context: any) => ReactNode; + itemContent: (index: number, data: any, context: any) => ReactNode; mobile?: boolean; } -const VirtualizedList = memo( - ({ mobile, dataSource, hideActionBar, itemContent }) => { - const virtuosoRef = useRef(null); - const [atBottom, setAtBottom] = useState(true); - const [isScrolling, setIsScrolling] = useState(false); +const VirtualizedList = memo(({ mobile, dataSource, itemContent }) => { + const virtuosoRef = useRef(null); + const [atBottom, setAtBottom] = useState(true); + const [isScrolling, setIsScrolling] = useState(false); - const [id, isFirstLoading, isCurrentChatLoaded] = useChatStore((s) => [ - chatSelectors.currentChatKey(s), - chatSelectors.currentChatLoadingState(s), - chatSelectors.isCurrentChatLoaded(s), - ]); + const [id, isFirstLoading, isCurrentChatLoaded] = useChatStore((s) => [ + chatSelectors.currentChatKey(s), + chatSelectors.currentChatLoadingState(s), + chatSelectors.isCurrentChatLoaded(s), + ]); - useEffect(() => { - if (virtuosoRef.current) { - virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' }); - } - }, [id]); + useEffect(() => { + if (virtuosoRef.current) { + virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' }); + } + }, [id]); - const prevDataLengthRef = useRef(dataSource.length); + const prevDataLengthRef = useRef(dataSource.length); - const getFollowOutput = useCallback(() => { - const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false; - prevDataLengthRef.current = dataSource.length; - return newFollowOutput; - }, [dataSource.length]); + const getFollowOutput = useCallback(() => { + const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false; + prevDataLengthRef.current = dataSource.length; + return newFollowOutput; + }, [dataSource.length]); - const theme = useTheme(); - // overscan should be 3 times the height of the window - const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0; + const theme = useTheme(); + // overscan should be 3 times the height of the window + const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0; - const defaultItemContent = useCallback( - (index: number, id: string) => , - [mobile, hideActionBar], - ); - - // first time loading or not loaded - if (isFirstLoading) return ; + // first time loading or not loaded + if (isFirstLoading) return ; - if (!isCurrentChatLoaded) - // use skeleton list when not loaded in server mode due to the loading duration is much longer than client mode - return isServerMode ? ( - - ) : ( - // in client mode and switch page, using the center loading for smooth transition -
- -
- ); - - return ( - - item} - data={dataSource} - followOutput={getFollowOutput} - increaseViewportBy={overscan} - initialTopMostItemIndex={dataSource?.length - 1} - isScrolling={setIsScrolling} - itemContent={itemContent ?? defaultItemContent} - overscan={overscan} - ref={virtuosoRef} - /> - { - const virtuoso = virtuosoRef.current; - switch (type) { - case 'auto': { - virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' }); - break; - } - case 'click': { - virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' }); - break; - } - } - }} + if (!isCurrentChatLoaded) + // use skeleton list when not loaded in server mode due to the loading duration is much longer than client mode + return isServerMode ? ( + + ) : ( + // in client mode and switch page, using the center loading for smooth transition +
+ - +
); - }, -); + + return ( + + item} + data={dataSource} + followOutput={getFollowOutput} + increaseViewportBy={overscan} + initialTopMostItemIndex={dataSource?.length - 1} + isScrolling={setIsScrolling} + itemContent={itemContent} + overscan={overscan} + ref={virtuosoRef} + /> + { + const virtuoso = virtuosoRef.current; + switch (type) { + case 'auto': { + virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' }); + break; + } + case 'click': { + virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' }); + break; + } + } + }} + /> + + ); +}); export default VirtualizedList; diff --git a/src/features/Conversation/index.ts b/src/features/Conversation/index.ts index 446c3ee40ebc..ca95bc7bd8ce 100644 --- a/src/features/Conversation/index.ts +++ b/src/features/Conversation/index.ts @@ -1,4 +1,3 @@ export { default as ChatItem } from './components/ChatItem'; -export { default as InboxWelcome } from './components/InboxWelcome'; export { default as SkeletonList } from './components/SkeletonList'; export { default as VirtualizedList } from './components/VirtualizedList'; diff --git a/src/features/Portal/Artifacts/index.ts b/src/features/Portal/Artifacts/index.ts index 4fc27b06b9f5..bc6f3d5fc554 100644 --- a/src/features/Portal/Artifacts/index.ts +++ b/src/features/Portal/Artifacts/index.ts @@ -5,6 +5,6 @@ import { useEnable } from './useEnable'; export const Artifacts: PortalImpl = { Body, - Header, + Title: Header, useEnable, }; diff --git a/src/features/Portal/FilePreview/index.ts b/src/features/Portal/FilePreview/index.ts index cb573c3822c9..eb50e5f885d1 100644 --- a/src/features/Portal/FilePreview/index.ts +++ b/src/features/Portal/FilePreview/index.ts @@ -5,6 +5,6 @@ import { useEnable } from './useEnable'; export const FilePreview: PortalImpl = { Body, - Header, + Title: Header, useEnable, }; diff --git a/src/features/Portal/Home/Header.tsx b/src/features/Portal/Home/Title.tsx similarity index 85% rename from src/features/Portal/Home/Header.tsx rename to src/features/Portal/Home/Title.tsx index d7331cc260ee..7a80da5ce439 100644 --- a/src/features/Portal/Home/Header.tsx +++ b/src/features/Portal/Home/Title.tsx @@ -4,7 +4,7 @@ import { Typography } from 'antd'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const Header = memo(() => { +const Title = memo(() => { const { t } = useTranslation('portal'); return ( @@ -14,4 +14,4 @@ const Header = memo(() => { ); }); -export default Header; +export default Title; diff --git a/src/features/Portal/Home/index.ts b/src/features/Portal/Home/index.ts index ddca2888bc96..dd481159ffd0 100644 --- a/src/features/Portal/Home/index.ts +++ b/src/features/Portal/Home/index.ts @@ -1,2 +1,2 @@ export { default as HomeBody } from './Body'; -export { default as HomeHeader } from './Header'; +export { default as HomeTitle } from './Title'; diff --git a/src/features/Portal/MessageDetail/index.ts b/src/features/Portal/MessageDetail/index.ts index 316924974c77..b116320fe5f7 100644 --- a/src/features/Portal/MessageDetail/index.ts +++ b/src/features/Portal/MessageDetail/index.ts @@ -5,6 +5,6 @@ import { useEnable } from './useEnable'; export const MessageDetail: PortalImpl = { Body, - Header, + Title: Header, useEnable, }; diff --git a/src/features/Portal/Plugins/index.ts b/src/features/Portal/Plugins/index.ts index 81933952019d..dcf171f3dd83 100644 --- a/src/features/Portal/Plugins/index.ts +++ b/src/features/Portal/Plugins/index.ts @@ -5,6 +5,6 @@ import { useEnable } from './useEnable'; export const Plugins: PortalImpl = { Body, - Header, + Title: Header, useEnable, }; diff --git a/src/features/Portal/components/Header.tsx b/src/features/Portal/components/Header.tsx new file mode 100644 index 000000000000..346267abc80e --- /dev/null +++ b/src/features/Portal/components/Header.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { ActionIcon } from '@lobehub/ui'; +import { XIcon } from 'lucide-react'; +import { ReactNode, memo } from 'react'; + +import SidebarHeader from '@/components/SidebarHeader'; +import { useChatStore } from '@/store/chat'; + +const Header = memo<{ title: ReactNode }>(({ title }) => { + const [toggleInspector] = useChatStore((s) => [s.togglePortal]); + + return ( + { + toggleInspector(false); + }} + /> + } + style={{ paddingBlock: 8, paddingInline: 8 }} + title={title} + /> + ); +}); + +export default Header; diff --git a/src/features/Portal/router.tsx b/src/features/Portal/router.tsx index 224190bdf005..2c7289e3312c 100644 --- a/src/features/Portal/router.tsx +++ b/src/features/Portal/router.tsx @@ -4,13 +4,32 @@ import { memo } from 'react'; import { Artifacts } from './Artifacts'; import { FilePreview } from './FilePreview'; -import { HomeBody, HomeHeader } from './Home'; +import { HomeBody, HomeTitle } from './Home'; import { MessageDetail } from './MessageDetail'; import { Plugins } from './Plugins'; +import Header from './components/Header'; import { PortalImpl } from './type'; const items: PortalImpl[] = [MessageDetail, Artifacts, Plugins, FilePreview]; +export const PortalTitle = memo(() => { + const enabledList: boolean[] = []; + + for (const item of items) { + const enabled = item.useEnable(); + enabledList.push(enabled); + } + + for (const [i, element] of enabledList.entries()) { + const Title = items[i].Title; + if (element) { + return ; + } + } + + return <HomeTitle />; +}); + export const PortalHeader = memo(() => { const enabledList: boolean[] = []; @@ -21,12 +40,12 @@ export const PortalHeader = memo(() => { for (const [i, element] of enabledList.entries()) { const Header = items[i].Header; - if (element) { + if (element && Header) { return <Header />; } } - return <HomeHeader />; + return <Header title={<PortalTitle />} />; }); const PortalBody = memo(() => { diff --git a/src/features/Portal/type.ts b/src/features/Portal/type.ts index 51b93f023233..32d7cdd27649 100644 --- a/src/features/Portal/type.ts +++ b/src/features/Portal/type.ts @@ -2,6 +2,8 @@ import { FC } from 'react'; export interface PortalImpl { Body: FC; - Header: FC; + Header?: FC; + Title: FC; + onClose?: () => void; useEnable: () => boolean; } diff --git a/src/features/Conversation/components/ChatList/index.tsx b/src/features/ShareModal/ShareImage/ChatList/index.tsx similarity index 70% rename from src/features/Conversation/components/ChatList/index.tsx rename to src/features/ShareModal/ShareImage/ChatList/index.tsx index 6f9bc121a8f0..f7801beca65b 100644 --- a/src/features/Conversation/components/ChatList/index.tsx +++ b/src/features/ShareModal/ShareImage/ChatList/index.tsx @@ -1,18 +1,17 @@ import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; +import { ChatItem } from '@/features/Conversation'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; -import Item from '../ChatItem'; - const ChatList = memo(() => { - const ids = useChatStore(chatSelectors.currentChatIDsWithGuideMessage); + const ids = useChatStore(chatSelectors.mainDisplayChatIDs); return ( <Flexbox height={'100%'} style={{ paddingTop: 24, position: 'relative' }}> {ids.map((id, index) => ( - <Item id={id} index={index} key={id} /> + <ChatItem id={id} index={index} key={id} /> ))} </Flexbox> ); diff --git a/src/features/ShareModal/ShareImage/Preview.tsx b/src/features/ShareModal/ShareImage/Preview.tsx index fb32f8f02f38..070b9f74af12 100644 --- a/src/features/ShareModal/ShareImage/Preview.tsx +++ b/src/features/ShareModal/ShareImage/Preview.tsx @@ -6,7 +6,6 @@ import { Flexbox } from 'react-layout-kit'; import PluginTag from '@/app/(main)/chat/(workspace)/features/PluginTag'; import { ProductLogo } from '@/components/Branding'; -import ChatList from '@/features/Conversation/components/ChatList'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useSessionStore } from '@/store/session'; @@ -14,6 +13,7 @@ import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selector import pkg from '../../../../package.json'; import { useContainerStyles } from '../style'; +import ChatList from './ChatList'; import { useStyles } from './style'; import { FieldType } from './type'; diff --git a/src/features/ShareModal/ShareJSON/index.tsx b/src/features/ShareModal/ShareJSON/index.tsx index d7dee23b1248..f110c67e50f8 100644 --- a/src/features/ShareModal/ShareJSON/index.tsx +++ b/src/features/ShareModal/ShareJSON/index.tsx @@ -46,7 +46,7 @@ const ShareImage = memo(() => { ]; const systemRole = useAgentStore(agentSelectors.currentAgentSystemRole); - const messages = useChatStore(chatSelectors.currentChats, isEqual); + const messages = useChatStore(chatSelectors.activeBaseChats, isEqual); const data = generateMessages({ ...fieldValue, messages, systemRole }); const content = JSON.stringify(data, null, 2); diff --git a/src/features/ShareModal/ShareText/index.tsx b/src/features/ShareModal/ShareText/index.tsx index 9644d3e9acb4..2142e65990d0 100644 --- a/src/features/ShareModal/ShareText/index.tsx +++ b/src/features/ShareModal/ShareText/index.tsx @@ -62,7 +62,7 @@ const ShareText = memo(() => { ]; const [systemRole] = useAgentStore((s) => [agentSelectors.currentAgentSystemRole(s)]); - const messages = useChatStore(chatSelectors.currentChats, isEqual); + const messages = useChatStore(chatSelectors.activeBaseChats, isEqual); const topic = useChatStore(topicSelectors.currentActiveTopic, isEqual); const title = topic?.title || t('shareModal.exportTitle'); diff --git a/src/layout/GlobalProvider/Debug.tsx b/src/layout/GlobalProvider/Debug.tsx new file mode 100644 index 000000000000..09a5b98aa37d --- /dev/null +++ b/src/layout/GlobalProvider/Debug.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import Script from 'next/script'; +import React, { memo } from 'react'; + +const Debug = memo(() => { + const searchParams = useSearchParams(); + + const debug = searchParams.get('debug'); + + return !!debug && <Script src="https://unpkg.com/react-scan/dist/auto.global.js" />; +}); + +export default Debug; diff --git a/src/layout/GlobalProvider/index.tsx b/src/layout/GlobalProvider/index.tsx index 45299cce5fec..79af38c11806 100644 --- a/src/layout/GlobalProvider/index.tsx +++ b/src/layout/GlobalProvider/index.tsx @@ -18,6 +18,7 @@ import { getAntdLocale } from '@/utils/locale'; import { isMobileDevice } from '@/utils/server/responsive'; import AppTheme from './AppTheme'; +import Debug from './Debug'; import Locale from './Locale'; import QueryProvider from './Query'; import StoreInitialization from './StoreInitialization'; @@ -85,6 +86,7 @@ const GlobalLayout = async ({ children }: PropsWithChildren) => { <StoreInitialization /> </ServerConfigStoreProvider> <DebugUI /> + <Debug /> </AppTheme> </Locale> </StyleRegistry> diff --git a/src/store/chat/slices/aiChat/actions/generateAIChat.ts b/src/store/chat/slices/aiChat/actions/generateAIChat.ts index 099d8e6fb51b..86dd6d871d9b 100644 --- a/src/store/chat/slices/aiChat/actions/generateAIChat.ts +++ b/src/store/chat/slices/aiChat/actions/generateAIChat.ts @@ -148,7 +148,7 @@ export const generateAIChat: StateCreator< // if autoCreateTopic is enabled, check to whether we need to create a topic if (!onlyAddUserMessage && !activeTopicId && agentConfig.enableAutoCreateTopic) { // check activeTopic and then auto create topic - const chats = chatSelectors.currentChats(get()); + const chats = chatSelectors.activeBaseChats(get()); // we will add two messages (user and assistant), so the finial length should +2 const featureLength = chats.length + 2; @@ -207,7 +207,7 @@ export const generateAIChat: StateCreator< } // Get the current messages to generate AI response - const messages = chatSelectors.currentChats(get()); + const messages = chatSelectors.mainDisplayChats(get()); const userFiles = chatSelectors.currentUserFiles(get()).map((f) => f.id); await internal_coreProcessMessage(messages, id, { @@ -223,7 +223,7 @@ export const generateAIChat: StateCreator< // check activeTopic and then auto update topic title if (newTopicId) { - const chats = chatSelectors.currentChats(get()); + const chats = chatSelectors.activeBaseChats(get()); await get().summaryTopicTitle(newTopicId, chats); return; } @@ -231,7 +231,7 @@ export const generateAIChat: StateCreator< const topic = topicSelectors.currentActiveTopic(get()); if (topic && !topic.title) { - const chats = chatSelectors.currentChats(get()); + const chats = chatSelectors.activeBaseChats(get()); await get().summaryTopicTitle(topic.id, chats); } }; @@ -484,7 +484,7 @@ export const generateAIChat: StateCreator< internal_resendMessage: async (messageId, traceId) => { // 1. 构造所有相关的历史记录 - const chats = chatSelectors.currentChats(get()); + const chats = chatSelectors.mainDisplayChats(get()); const currentIndex = chats.findIndex((c) => c.id === messageId); if (currentIndex < 0) return; diff --git a/src/store/chat/slices/message/action.ts b/src/store/chat/slices/message/action.ts index 969b10514869..7e7cdefb7ea3 100644 --- a/src/store/chat/slices/message/action.ts +++ b/src/store/chat/slices/message/action.ts @@ -128,7 +128,7 @@ export const chatMessage: StateCreator< if (message.tools) { const toolMessageIds = message.tools.flatMap((tool) => { const messages = chatSelectors - .currentChats(get()) + .activeBaseChats(get()) .filter((m) => m.tool_call_id === tool.id); return messages.map((m) => m.id); @@ -252,7 +252,7 @@ export const chatMessage: StateCreator< if (!activeId) return; - const messages = messagesReducer(chatSelectors.currentChats(get()), payload); + const messages = messagesReducer(chatSelectors.activeBaseChats(get()), payload); const nextMap = { ...get().messagesMap, [chatSelectors.currentChatKey(get())]: messages }; diff --git a/src/store/chat/slices/message/selectors.test.ts b/src/store/chat/slices/message/selectors.test.ts index cb02232989f5..c9e9994683c0 100644 --- a/src/store/chat/slices/message/selectors.test.ts +++ b/src/store/chat/slices/message/selectors.test.ts @@ -232,62 +232,6 @@ describe('chatSelectors', () => { }); }); - describe('currentChatsWithGuideMessage', () => { - it('should return existing messages except tool message', () => { - const state = merge(initialStore, { - messagesMap: { - [messageMapKey('someActiveId')]: mockMessages, - }, - activeId: 'someActiveId', - }); - const chats = chatSelectors.currentChatsWithGuideMessage({} as MetaData)(state); - expect(chats).toEqual(mockedChats.slice(0, 2)); - }); - - it('should add a guide message if the chat is brand new', () => { - const state = merge(initialStore, { messages: [], activeId: 'someActiveId' }); - const metaData = { title: 'Mock Agent', description: 'Mock Description' }; - - const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state); - - expect(chats).toHaveLength(1); - expect(chats[0].content).toBeDefined(); - expect(chats[0].meta.avatar).toEqual(DEFAULT_INBOX_AVATAR); - expect(chats[0].meta).toEqual(expect.objectContaining(metaData)); - }); - - it('should use inbox message for INBOX_SESSION_ID', () => { - const state = merge(initialStore, { messages: [], activeId: INBOX_SESSION_ID }); - const metaData = { title: 'Mock Agent', description: 'Mock Description' }; - - const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state); - - expect(chats[0].content).toEqual(''); // Assuming translation returns a string containing this - }); - - it('should use agent default message for non-inbox sessions', () => { - const state = merge(initialStore, { messages: [], activeId: 'someActiveId' }); - const metaData = { title: 'Mock Agent' }; - - const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state); - - expect(chats[0].content).toMatch('agentDefaultMessage'); // Assuming translation returns a string containing this - }); - - it('should use agent default message without edit button for non-inbox sessions when agent is not editable', () => { - act(() => { - createServerConfigStore().setState({ featureFlags: { edit_agent: false } }); - }); - - const state = merge(initialStore, { messages: [], activeId: 'someActiveId' }); - const metaData = { title: 'Mock Agent' }; - - const chats = chatSelectors.currentChatsWithGuideMessage(metaData)(state); - - expect(chats[0].content).toMatch('agentDefaultMessageWithoutEdit'); - }); - }); - describe('chatsMessageString', () => { it('should concatenate the contents of all messages returned by currentChatsWithHistoryConfig', () => { // Prepare a state with a few messages @@ -415,36 +359,6 @@ describe('chatSelectors', () => { }); }); - describe('currentChatIDsWithGuideMessage', () => { - it('should return message IDs including guide message for empty chat', () => { - const state: Partial<ChatStore> = { - activeId: 'test-id', - messagesMap: { - [messageMapKey('test-id')]: [], - }, - }; - const result = chatSelectors.currentChatIDsWithGuideMessage(state as ChatStore); - expect(result).toHaveLength(1); - expect(result[0]).toBe('default'); - }); - - it('should return existing message IDs for non-empty chat', () => { - const messages = [ - { id: '1', role: 'user', content: 'Hello' }, - { id: '2', role: 'assistant', content: 'Hi' }, - ] as ChatMessage[]; - const state: Partial<ChatStore> = { - activeId: 'test-id', - messagesMap: { - [messageMapKey('test-id')]: messages, - }, - }; - const result = chatSelectors.currentChatIDsWithGuideMessage(state as ChatStore); - expect(result).toHaveLength(2); - expect(result).toEqual(['1', '2']); - }); - }); - describe('isToolCallStreaming', () => { it('should return true when tool call is streaming for given message and index', () => { const state: Partial<ChatStore> = { diff --git a/src/store/chat/slices/message/selectors.ts b/src/store/chat/slices/message/selectors.ts index d1d86dfe6540..450d15408472 100644 --- a/src/store/chat/slices/message/selectors.ts +++ b/src/store/chat/slices/message/selectors.ts @@ -1,19 +1,13 @@ -import { t } from 'i18next'; - -import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta'; +import { DEFAULT_USER_AVATAR } from '@/const/meta'; import { INBOX_SESSION_ID } from '@/const/session'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { messageMapKey } from '@/store/chat/utils/messageMapKey'; -import { featureFlagsSelectors } from '@/store/serverConfig'; -import { createServerConfigStore } from '@/store/serverConfig/store'; import { useSessionStore } from '@/store/session'; import { sessionMetaSelectors } from '@/store/session/selectors'; import { useUserStore } from '@/store/user'; import { userProfileSelectors } from '@/store/user/selectors'; import { ChatFileItem, ChatMessage } from '@/types/message'; -import { MetaData } from '@/types/meta'; -import { merge } from '@/utils/merge'; import { chatHelpers } from '../../helpers'; import type { ChatStoreState } from '../../initialState'; @@ -38,8 +32,10 @@ const getMeta = (message: ChatMessage) => { const currentChatKey = (s: ChatStoreState) => messageMapKey(s.activeId, s.activeTopicId); -// 当前激活的消息列表 -const currentChats = (s: ChatStoreState): ChatMessage[] => { +/** + * Current active raw message list, include thread messages + */ +const activeBaseChats = (s: ChatStoreState): ChatMessage[] => { if (!s.activeId) return []; const messages = s.messagesMap[currentChatKey(s)] || []; @@ -47,14 +43,54 @@ const currentChats = (s: ChatStoreState): ChatMessage[] => { return messages.map((i) => ({ ...i, meta: getMeta(i) })); }; +/** + * 排除掉所有 tool 消息,在展示时需要使用 + */ +const activeBaseChatsWithoutTool = (s: ChatStoreState) => { + const messages = activeBaseChats(s); + + return messages.filter((m) => m.role !== 'tool'); +}; + +/** + * Main display chats + * 根据当前不同的状态,返回不同的消息列表 + */ +const mainDisplayChats = (s: ChatStoreState): ChatMessage[] => { + // 如果没有 activeThreadId,则返回所有的主消息 + return activeBaseChats(s); + // const mains = activeBaseChats(s).filter((m) => !m.threadId); + // if (!s.activeThreadId) return mains; + // + // const thread = s.threadMaps[s.activeTopicId!]?.find((t) => t.id === s.activeThreadId); + // + // if (!thread) return mains; + // + // const sourceIndex = mains.findIndex((m) => m.id === thread.sourceMessageId); + // const sliced = mains.slice(0, sourceIndex + 1); + // + // return [...sliced, ...activeBaseChats(s).filter((m) => m.threadId === s.activeThreadId)]; +}; + +const mainDisplayChatIDs = (s: ChatStoreState) => { + return mainDisplayChats(s).map((s) => s.id); +}; + +const currentChatsWithHistoryConfig = (s: ChatStoreState): ChatMessage[] => { + const chats = activeBaseChats(s); + const config = agentSelectors.currentAgentChatConfig(useAgentStore.getState()); + + return chatHelpers.getSlicedMessagesWithConfig(chats, config); +}; + const currentToolMessages = (s: ChatStoreState) => { - const messages = currentChats(s); + const messages = activeBaseChats(s); return messages.filter((m) => m.role === 'tool'); }; const currentUserMessages = (s: ChatStoreState) => { - const messages = currentChats(s); + const messages = activeBaseChats(s); return messages.filter((m) => m.role === 'user'); }; @@ -68,84 +104,29 @@ const currentUserFiles = (s: ChatStoreState) => { .filter(Boolean) as ChatFileItem[]; }; -const initTime = Date.now(); - const showInboxWelcome = (s: ChatStoreState): boolean => { const isInbox = s.activeId === INBOX_SESSION_ID; if (!isInbox) return false; - const data = currentChats(s); + const data = activeBaseChats(s); return data.length === 0; }; -// Custom message for new assistant initialization -const currentChatsWithGuideMessage = - (meta: MetaData) => - (s: ChatStoreState): ChatMessage[] => { - // skip tool message - const data = currentChats(s).filter((m) => m.role !== 'tool'); - - const { isAgentEditable } = featureFlagsSelectors(createServerConfigStore().getState()); - - const isBrandNewChat = data.length === 0; - - if (!isBrandNewChat) return data; - - const [activeId, isInbox] = [s.activeId, s.activeId === INBOX_SESSION_ID]; - - const inboxMsg = ''; - const agentSystemRoleMsg = t('agentDefaultMessageWithSystemRole', { - name: meta.title || t('defaultAgent'), - ns: 'chat', - systemRole: meta.description, - }); - const agentMsg = t(isAgentEditable ? 'agentDefaultMessage' : 'agentDefaultMessageWithoutEdit', { - name: meta.title || t('defaultAgent'), - ns: 'chat', - url: `/chat/settings?session=${activeId}`, - }); - - const emptyInboxGuideMessage = { - content: isInbox ? inboxMsg : !!meta.description ? agentSystemRoleMsg : agentMsg, - createdAt: initTime, - extra: {}, - id: 'default', - meta: merge({ avatar: DEFAULT_INBOX_AVATAR }, meta), - role: 'assistant', - updatedAt: initTime, - } as ChatMessage; - - return [emptyInboxGuideMessage]; - }; - -const currentChatIDsWithGuideMessage = (s: ChatStoreState) => { - const meta = sessionMetaSelectors.currentAgentMeta(useSessionStore.getState()); - - return currentChatsWithGuideMessage(meta)(s).map((s) => s.id); -}; - -const currentChatsWithHistoryConfig = (s: ChatStoreState): ChatMessage[] => { - const chats = currentChats(s); - const config = agentSelectors.currentAgentChatConfig(useAgentStore.getState()); - - return chatHelpers.getSlicedMessagesWithConfig(chats, config); -}; - const chatsMessageString = (s: ChatStoreState): string => { const chats = currentChatsWithHistoryConfig(s); return chats.map((m) => m.content).join(''); }; const getMessageById = (id: string) => (s: ChatStoreState) => - chatHelpers.getMessageById(currentChats(s), id); + chatHelpers.getMessageById(activeBaseChats(s), id); const getMessageByToolCallId = (id: string) => (s: ChatStoreState) => { - const messages = currentChats(s); + const messages = activeBaseChats(s); return messages.find((m) => m.tool_call_id === id); }; const getTraceIdByMessageId = (id: string) => (s: ChatStoreState) => getMessageById(id)(s)?.traceId; -const latestMessage = (s: ChatStoreState) => currentChats(s).at(-1); +const latestMessage = (s: ChatStoreState) => activeBaseChats(s).at(-1); const currentChatLoadingState = (s: ChatStoreState) => !s.messagesInit; @@ -187,12 +168,11 @@ const isSendButtonDisabledByMessage = (s: ChatStoreState) => isInRAGFlow(s); export const chatSelectors = { + activeBaseChats, + activeBaseChatsWithoutTool, chatsMessageString, - currentChatIDsWithGuideMessage, currentChatKey, currentChatLoadingState, - currentChats, - currentChatsWithGuideMessage, currentChatsWithHistoryConfig, currentToolMessages, currentUserFiles, @@ -211,5 +191,7 @@ export const chatSelectors = { isSendButtonDisabledByMessage, isToolCallStreaming, latestMessage, + mainDisplayChatIDs, + mainDisplayChats, showInboxWelcome, }; diff --git a/src/store/chat/slices/plugin/action.test.ts b/src/store/chat/slices/plugin/action.test.ts index b4970d57e058..1abc9bcb94ce 100644 --- a/src/store/chat/slices/plugin/action.test.ts +++ b/src/store/chat/slices/plugin/action.test.ts @@ -149,7 +149,7 @@ describe('ChatPluginAction', () => { it('should update message content and trigger the ai message', async () => { // 设置模拟函数的返回值 const mockCurrentChats: any[] = []; - vi.spyOn(chatSelectors, 'currentChats').mockReturnValue(mockCurrentChats); + vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue(mockCurrentChats); // 设置初始状态 const initialState = { @@ -184,7 +184,7 @@ describe('ChatPluginAction', () => { it('should update message content and not trigger ai message', async () => { // 设置模拟函数的返回值 const mockCurrentChats: any[] = []; - vi.spyOn(chatSelectors, 'currentChats').mockReturnValue(mockCurrentChats); + vi.spyOn(chatSelectors, 'activeBaseChats').mockReturnValue(mockCurrentChats); // 设置初始状态 const initialState = { diff --git a/src/store/chat/slices/plugin/action.ts b/src/store/chat/slices/plugin/action.ts index 4ad9844647a3..1b90c7c406e6 100644 --- a/src/store/chat/slices/plugin/action.ts +++ b/src/store/chat/slices/plugin/action.ts @@ -202,7 +202,7 @@ export const chatPlugin: StateCreator< triggerAIMessage: async ({ parentId, traceId }) => { const { internal_coreProcessMessage } = get(); - const chats = chatSelectors.currentChats(get()); + const chats = chatSelectors.activeBaseChats(get()); await internal_coreProcessMessage(chats, parentId ?? chats.at(-1)!.id, { traceId }); }, diff --git a/src/store/chat/slices/topic/action.ts b/src/store/chat/slices/topic/action.ts index fe38228b2c46..7aa7234fe669 100644 --- a/src/store/chat/slices/topic/action.ts +++ b/src/store/chat/slices/topic/action.ts @@ -79,7 +79,7 @@ export const chatTopic: StateCreator< createTopic: async () => { const { activeId, internal_createTopic } = get(); - const messages = chatSelectors.currentChats(get()); + const messages = chatSelectors.activeBaseChats(get()); set({ creatingTopic: true }, false, n('creatingTopic/start')); const topicId = await internal_createTopic({ @@ -94,7 +94,7 @@ export const chatTopic: StateCreator< saveToTopic: async () => { // if there is no message, stop - const messages = chatSelectors.currentChats(get()); + const messages = chatSelectors.activeBaseChats(get()); if (messages.length === 0) return; const { activeId, summaryTopicTitle, internal_createTopic } = get();