Skip to content

Commit

Permalink
🐛 fix: fix can not stop generating (lobehub#5671)
Browse files Browse the repository at this point in the history
* improve thinking style

* fix cannot stop with thinking

* clean

* improve lobe thinking

* fix portal footer

* fix style

* memo portal width
  • Loading branch information
arvinxx authored Feb 2, 2025
1 parent 355e275 commit ae39c35
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 93 deletions.
1 change: 1 addition & 0 deletions src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Layout = ({ children }: PropsWithChildren) => {
<Modal
allowFullscreen
className={cx(isPortalThread && styles.container)}
footer={null}
height={'95%'}
onCancel={() => togglePortal(false)}
open={showMobilePortal}
Expand Down
28 changes: 26 additions & 2 deletions src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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`
Expand Down Expand Up @@ -49,23 +52,44 @@ 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 && (
<DraggablePanel
className={styles.drawer}
classNames={{
content: styles.content,
}}
defaultSize={{ width: tmpWidth }}
expand
hanlderStyle={{ display: 'none' }}
maxWidth={CHAT_PORTAL_MAX_WIDTH}
minWidth={
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 }}
>
<DraggablePanelContainer
style={{
Expand Down
30 changes: 14 additions & 16 deletions src/components/Thinking/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createStyles } from 'antd-style';
import { AnimatePresence, motion } from 'framer-motion';
import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
import { rgba } from 'polished';
import { memo, useEffect, useState } from 'react';
import { CSSProperties, memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

Expand Down Expand Up @@ -61,49 +61,47 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
interface ThinkingProps {
content?: string;
duration?: number;
style?: CSSProperties;
thinking?: boolean;
}

const Thinking = memo<ThinkingProps>(({ content, duration, thinking }) => {
const Thinking = memo<ThinkingProps>(({ 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 (
<Flexbox className={cx(styles.container, showDetail && styles.expand)} gap={16}>
<Flexbox className={cx(styles.container, showDetail && styles.expand)} gap={16} style={style}>
<Flexbox
distribution={'space-between'}
flex={1}
gap={8}
horizontal
onClick={() => {
setShowDetail(!showDetail);
}}
style={{ cursor: 'pointer' }}
>
{thinking ? (
<Flexbox gap={8} horizontal>
<Flexbox align={'center'} gap={8} horizontal>
<Icon icon={AtomIcon} />
<Flexbox className={styles.shinyText} horizontal>
{t('Thinking.thinking')}
</Flexbox>
</Flexbox>
) : (
<Flexbox gap={8} horizontal>
<Flexbox align={'center'} gap={8} horizontal>
<Icon icon={AtomIcon} />
{!duration
? t('Thinking.thoughtWithDuration')
: t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })}
<Flexbox>
{!duration
? t('Thinking.thoughtWithDuration')
: t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })}
</Flexbox>
</Flexbox>
)}
<Flexbox gap={4} horizontal>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<MarkdownElementProps>(({ 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 (
<Flexbox
className={styles.container}
gap={16}
onClick={() => {
setShowDetail(!showDetail);
}}
width={'100%'}
>
<Flexbox distribution={'space-between'} flex={1} horizontal>
<Flexbox gap={8} horizontal>
<Icon icon={isGenerating ? Loader2Icon : BringToFrontIcon} spin={isGenerating} />
{isGenerating ? (
<span className={cx(dotLoading)}>{t('artifact.thinking')}</span>
) : (
t('artifact.thought')
)}
</Flexbox>
<Icon icon={expand ? ChevronDown : ChevronRight} />
</Flexbox>
{expand && children}
</Flexbox>
<Thinking
content={children as string}
style={{ width: isGenerating ? '100%' : undefined }}
thinking={isGenerating}
/>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ describe('chatMessage actions', () => {
const abortController = new AbortController();

act(() => {
useChatStore.setState({ abortController });
useChatStore.setState({ chatLoadingIdsAbortController: abortController });
});

await act(async () => {
Expand All @@ -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());
Expand All @@ -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());

Expand All @@ -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');
Expand Down Expand Up @@ -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']);
});

Expand All @@ -887,7 +887,7 @@ describe('chatMessage actions', () => {
});

const state = useChatStore.getState();
expect(state.abortController).toBeUndefined();
expect(state.chatLoadingIdsAbortController).toBeUndefined();
expect(state.chatLoadingIds).toEqual([]);
});

Expand Down Expand Up @@ -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);
});
});

Expand Down
7 changes: 4 additions & 3 deletions src/store/chat/slices/aiChat/actions/generateAIChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
2 changes: 1 addition & 1 deletion src/store/chat/slices/aiChat/initialState.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export interface ChatAIChatState {
abortController?: AbortController;
/**
* is the AI message is generating
*/
chatLoadingIds: string[];
chatLoadingIdsAbortController?: AbortController;
inputFiles: File[];
inputMessage: string;
/**
Expand Down
7 changes: 4 additions & 3 deletions src/store/chat/slices/message/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/store/global/initialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface SystemStatus {
latestChangelogId?: string;
mobileShowPortal?: boolean;
mobileShowTopic?: boolean;
portalWidth: number;
sessionsWidth: number;
showChatSideBar?: boolean;
showFilePanel?: boolean;
Expand Down Expand Up @@ -86,6 +87,7 @@ export const INITIAL_STATUS = {
hideThreadLimitAlert: false,
inputHeight: 200,
mobileShowTopic: false,
portalWidth: 400,
sessionsWidth: 320,
showChatSideBar: true,
showFilePanel: true,
Expand Down
Loading

0 comments on commit ae39c35

Please sign in to comment.