From ae39c3593b84a77d00b6a86d0846925051381f6a Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sun, 2 Feb 2025 19:07:09 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20can=20not=20stop=20?= =?UTF-8?q?generating=20(#5671)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve thinking style * fix cannot stop with thinking * clean * improve lobe thinking * fix portal footer * fix style * memo portal width --- .../(workspace)/@portal/_layout/Mobile.tsx | 1 + .../(workspace)/_layout/Desktop/Portal.tsx | 28 +++++++- src/components/Thinking/index.tsx | 30 ++++----- .../MarkdownElements/LobeThinking/Render.tsx | 65 ++----------------- .../actions/__tests__/generateAIChat.test.ts | 20 +++--- .../slices/aiChat/actions/generateAIChat.ts | 7 +- src/store/chat/slices/aiChat/initialState.ts | 2 +- src/store/chat/slices/message/action.ts | 7 +- src/store/global/initialState.ts | 2 + src/store/global/selectors.ts | 2 + 10 files changed, 71 insertions(+), 93 deletions(-) diff --git a/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx b/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx index a12e6c7a0ebd3..d94a92fc1bdf9 100644 --- a/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx +++ b/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx @@ -28,6 +28,7 @@ const Layout = ({ children }: PropsWithChildren) => { togglePortal(false)} open={showMobilePortal} diff --git a/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx b/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx index 9078318088334..a342126c4e436 100644 --- a/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +++ b/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx @@ -1,9 +1,10 @@ 'use client'; -import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui'; +import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui'; import { createStyles, useResponsive } from 'antd-style'; +import isEqual from 'fast-deep-equal'; import { rgba } from 'polished'; -import { PropsWithChildren, memo } from 'react'; +import { PropsWithChildren, memo, useState } from 'react'; import { Flexbox } from 'react-layout-kit'; import { @@ -13,6 +14,8 @@ import { } from '@/const/layoutTokens'; import { useChatStore } from '@/store/chat'; import { chatPortalSelectors, portalThreadSelectors } from '@/store/chat/selectors'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; const useStyles = createStyles(({ css, token, isDarkMode }) => ({ content: css` @@ -49,6 +52,24 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => { portalThreadSelectors.showThread(s), ]); + const [portalWidth, updateSystemStatus] = useGlobalStore((s) => [ + systemStatusSelectors.portalWidth(s), + s.updateSystemStatus, + ]); + + const [tmpWidth, setWidth] = useState(portalWidth); + if (tmpWidth !== portalWidth) setWidth(portalWidth); + + const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => { + if (!size) return; + const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width; + if (!nextWidth) return; + + if (isEqual(nextWidth, portalWidth)) return; + setWidth(nextWidth); + updateSystemStatus({ portalWidth: nextWidth }); + }; + return ( showInspector && ( { classNames={{ content: styles.content, }} + defaultSize={{ width: tmpWidth }} expand hanlderStyle={{ display: 'none' }} maxWidth={CHAT_PORTAL_MAX_WIDTH} @@ -63,9 +85,11 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => { showArtifactUI || showToolUI || showThread ? CHAT_PORTAL_TOOL_UI_WIDTH : CHAT_PORTAL_WIDTH } mode={md ? 'fixed' : 'float'} + onSizeChange={handleSizeChange} placement={'right'} showHandlerWhenUnexpand={false} showHandlerWideArea={false} + size={{ height: '100%', width: portalWidth }} > ({ interface ThinkingProps { content?: string; duration?: number; + style?: CSSProperties; thinking?: boolean; } -const Thinking = memo(({ content, duration, thinking }) => { +const Thinking = memo(({ content, duration, thinking, style }) => { const { t } = useTranslation(['components', 'common']); const { styles, cx } = useStyles(); const [showDetail, setShowDetail] = useState(false); useEffect(() => { - if (thinking && !content) { - setShowDetail(true); - } - - if (!thinking) { - setShowDetail(false); - } - }, [thinking, content]); + setShowDetail(!!thinking); + }, [thinking]); return ( - + { setShowDetail(!showDetail); @@ -92,18 +88,20 @@ const Thinking = memo(({ content, duration, thinking }) => { style={{ cursor: 'pointer' }} > {thinking ? ( - + {t('Thinking.thinking')} ) : ( - + - {!duration - ? t('Thinking.thoughtWithDuration') - : t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })} + + {!duration + ? t('Thinking.thoughtWithDuration') + : t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })} + )} diff --git a/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx b/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx index c29762f446134..3d8d30df6d2d7 100644 --- a/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx +++ b/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx @@ -1,14 +1,9 @@ -import { Icon } from '@lobehub/ui'; -import { createStyles } from 'antd-style'; -import { BringToFrontIcon, ChevronDown, ChevronRight, Loader2Icon } from 'lucide-react'; -import { memo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Flexbox } from 'react-layout-kit'; +import { memo } from 'react'; +import Thinking from '@/components/Thinking'; import { ARTIFACT_THINKING_TAG } from '@/const/plugin'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; -import { dotLoading } from '@/styles/loading'; import { MarkdownElementProps } from '../type'; @@ -22,64 +17,18 @@ export const isLobeThinkingClosed = (input: string = '') => { return input.includes(openTag) && input.includes(closeTag); }; -const useStyles = createStyles(({ css, token }) => ({ - container: css` - cursor: pointer; - - padding-block: 8px; - padding-inline: 12px; - padding-inline-end: 12px; - border-radius: 8px; - - color: ${token.colorText}; - - background: ${token.colorFillQuaternary}; - `, - title: css` - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; - - font-size: 12px; - text-overflow: ellipsis; - `, -})); - const Render = memo(({ children, id }) => { - const { t } = useTranslation('chat'); - const { styles, cx } = useStyles(); - const [isGenerating] = useChatStore((s) => { const message = chatSelectors.getMessageById(id)(s); return [!isLobeThinkingClosed(message?.content)]; }); - const [showDetail, setShowDetail] = useState(false); - - const expand = showDetail || isGenerating; return ( - { - setShowDetail(!showDetail); - }} - width={'100%'} - > - - - - {isGenerating ? ( - {t('artifact.thinking')} - ) : ( - t('artifact.thought') - )} - - - - {expand && children} - + ); }); diff --git a/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts b/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts index 05e1d9a4347d2..67856b5687e82 100644 --- a/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +++ b/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts @@ -576,7 +576,7 @@ describe('chatMessage actions', () => { const abortController = new AbortController(); act(() => { - useChatStore.setState({ abortController }); + useChatStore.setState({ chatLoadingIdsAbortController: abortController }); }); await act(async () => { @@ -596,18 +596,18 @@ describe('chatMessage actions', () => { await act(async () => { // 确保没有设置 abortController - useChatStore.setState({ abortController: undefined }); + useChatStore.setState({ chatLoadingIdsAbortController: undefined }); result.current.stopGenerateMessage(); }); // 由于没有 abortController,不应调用任何方法 - expect(result.current.abortController).toBeUndefined(); + expect(result.current.chatLoadingIdsAbortController).toBeUndefined(); }); it('should return early if abortController is undefined', () => { act(() => { - useChatStore.setState({ abortController: undefined }); + useChatStore.setState({ chatLoadingIdsAbortController: undefined }); }); const { result } = renderHook(() => useChatStore()); @@ -625,7 +625,7 @@ describe('chatMessage actions', () => { const abortMock = vi.fn(); const abortController = { abort: abortMock } as unknown as AbortController; act(() => { - useChatStore.setState({ abortController }); + useChatStore.setState({ chatLoadingIdsAbortController: abortController }); }); const { result } = renderHook(() => useChatStore()); @@ -639,7 +639,7 @@ describe('chatMessage actions', () => { it('should call internal_toggleChatLoading with correct parameters', () => { const abortController = new AbortController(); act(() => { - useChatStore.setState({ abortController }); + useChatStore.setState({ chatLoadingIdsAbortController: abortController }); }); const { result } = renderHook(() => useChatStore()); const spy = vi.spyOn(result.current, 'internal_toggleChatLoading'); @@ -868,7 +868,7 @@ describe('chatMessage actions', () => { }); const state = useChatStore.getState(); - expect(state.abortController).toBeInstanceOf(AbortController); + expect(state.chatLoadingIdsAbortController).toBeInstanceOf(AbortController); expect(state.chatLoadingIds).toEqual(['message-id']); }); @@ -887,7 +887,7 @@ describe('chatMessage actions', () => { }); const state = useChatStore.getState(); - expect(state.abortController).toBeUndefined(); + expect(state.chatLoadingIdsAbortController).toBeUndefined(); expect(state.chatLoadingIds).toEqual([]); }); @@ -920,12 +920,12 @@ describe('chatMessage actions', () => { const abortController = new AbortController(); act(() => { - useChatStore.setState({ abortController }); + useChatStore.setState({ chatLoadingIdsAbortController: abortController }); result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action'); }); const state = useChatStore.getState(); - expect(state.abortController).toEqual(abortController); + expect(state.chatLoadingIdsAbortController).toEqual(abortController); }); }); diff --git a/src/store/chat/slices/aiChat/actions/generateAIChat.ts b/src/store/chat/slices/aiChat/actions/generateAIChat.ts index 77cd04fca49a7..106af8157a03f 100644 --- a/src/store/chat/slices/aiChat/actions/generateAIChat.ts +++ b/src/store/chat/slices/aiChat/actions/generateAIChat.ts @@ -267,10 +267,11 @@ export const generateAIChat: StateCreator< await Promise.all([summaryTitle(), addFilesToAgent()]); }, stopGenerateMessage: () => { - const { abortController, internal_toggleChatLoading } = get(); - if (!abortController) return; + const { chatLoadingIdsAbortController, internal_toggleChatLoading } = get(); - abortController.abort(MESSAGE_CANCEL_FLAT); + if (!chatLoadingIdsAbortController) return; + + chatLoadingIdsAbortController.abort(MESSAGE_CANCEL_FLAT); internal_toggleChatLoading(false, undefined, n('stopGenerateMessage') as string); }, diff --git a/src/store/chat/slices/aiChat/initialState.ts b/src/store/chat/slices/aiChat/initialState.ts index 59ec88a82719e..6d799714efe26 100644 --- a/src/store/chat/slices/aiChat/initialState.ts +++ b/src/store/chat/slices/aiChat/initialState.ts @@ -1,9 +1,9 @@ export interface ChatAIChatState { - abortController?: AbortController; /** * is the AI message is generating */ chatLoadingIds: string[]; + chatLoadingIdsAbortController?: AbortController; inputFiles: File[]; inputMessage: string; /** diff --git a/src/store/chat/slices/message/action.ts b/src/store/chat/slices/message/action.ts index 9411c78f83ceb..383e53e11c292 100644 --- a/src/store/chat/slices/message/action.ts +++ b/src/store/chat/slices/message/action.ts @@ -368,13 +368,14 @@ export const chatMessage: StateCreator< ); }, internal_toggleLoadingArrays: (key, loading, id, action) => { + const abortControllerKey = `${key}AbortController`; if (loading) { window.addEventListener('beforeunload', preventLeavingFn); const abortController = new AbortController(); set( { - abortController, + [abortControllerKey]: abortController, [key]: toggleBooleanList(get()[key] as string[], id!, loading), }, false, @@ -384,11 +385,11 @@ export const chatMessage: StateCreator< return abortController; } else { if (!id) { - set({ abortController: undefined, [key]: [] }, false, action); + set({ [abortControllerKey]: undefined, [key]: [] }, false, action); } else set( { - abortController: undefined, + [abortControllerKey]: undefined, [key]: toggleBooleanList(get()[key] as string[], id, loading), }, false, diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 9cb79206dbe6c..11e67eb7c6d50 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -52,6 +52,7 @@ export interface SystemStatus { latestChangelogId?: string; mobileShowPortal?: boolean; mobileShowTopic?: boolean; + portalWidth: number; sessionsWidth: number; showChatSideBar?: boolean; showFilePanel?: boolean; @@ -86,6 +87,7 @@ export const INITIAL_STATUS = { hideThreadLimitAlert: false, inputHeight: 200, mobileShowTopic: false, + portalWidth: 400, sessionsWidth: 320, showChatSideBar: true, showFilePanel: true, diff --git a/src/store/global/selectors.ts b/src/store/global/selectors.ts index f75087dfab322..0f75cbda4625f 100644 --- a/src/store/global/selectors.ts +++ b/src/store/global/selectors.ts @@ -20,6 +20,7 @@ const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller; const showChatHeader = (s: GlobalStore) => !s.status.zenMode; const inZenMode = (s: GlobalStore) => s.status.zenMode; const sessionWidth = (s: GlobalStore) => s.status.sessionsWidth; +const portalWidth = (s: GlobalStore) => s.status.portalWidth || 400; const filePanelWidth = (s: GlobalStore) => s.status.filePanelWidth; const inputHeight = (s: GlobalStore) => s.status.inputHeight; const threadInputHeight = (s: GlobalStore) => s.status.threadInputHeight; @@ -59,6 +60,7 @@ export const systemStatusSelectors = { isPgliteNotInited, mobileShowPortal, mobileShowTopic, + portalWidth, sessionGroupKeys, sessionWidth, showChatHeader,