@lobehub/chat 1.49.11 → 1.49.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.49.12](https://github.com/lobehub/lobe-chat/compare/v1.49.11...v1.49.12)
6
+
7
+ <sup>Released on **2025-02-02**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix can not stop generating.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix can not stop generating, closes [#5671](https://github.com/lobehub/lobe-chat/issues/5671) ([ae39c35](https://github.com/lobehub/lobe-chat/commit/ae39c35))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.49.11](https://github.com/lobehub/lobe-chat/compare/v1.49.10...v1.49.11)
6
31
 
7
32
  <sup>Released on **2025-02-02**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix can not stop generating."
6
+ ]
7
+ },
8
+ "date": "2025-02-02",
9
+ "version": "1.49.12"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.49.11",
3
+ "version": "1.49.12",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -28,6 +28,7 @@ const Layout = ({ children }: PropsWithChildren) => {
28
28
  <Modal
29
29
  allowFullscreen
30
30
  className={cx(isPortalThread && styles.container)}
31
+ footer={null}
31
32
  height={'95%'}
32
33
  onCancel={() => togglePortal(false)}
33
34
  open={showMobilePortal}
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui';
3
+ import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui';
4
4
  import { createStyles, useResponsive } from 'antd-style';
5
+ import isEqual from 'fast-deep-equal';
5
6
  import { rgba } from 'polished';
6
- import { PropsWithChildren, memo } from 'react';
7
+ import { PropsWithChildren, memo, useState } from 'react';
7
8
  import { Flexbox } from 'react-layout-kit';
8
9
 
9
10
  import {
@@ -13,6 +14,8 @@ import {
13
14
  } from '@/const/layoutTokens';
14
15
  import { useChatStore } from '@/store/chat';
15
16
  import { chatPortalSelectors, portalThreadSelectors } from '@/store/chat/selectors';
17
+ import { useGlobalStore } from '@/store/global';
18
+ import { systemStatusSelectors } from '@/store/global/selectors';
16
19
 
17
20
  const useStyles = createStyles(({ css, token, isDarkMode }) => ({
18
21
  content: css`
@@ -49,6 +52,24 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => {
49
52
  portalThreadSelectors.showThread(s),
50
53
  ]);
51
54
 
55
+ const [portalWidth, updateSystemStatus] = useGlobalStore((s) => [
56
+ systemStatusSelectors.portalWidth(s),
57
+ s.updateSystemStatus,
58
+ ]);
59
+
60
+ const [tmpWidth, setWidth] = useState(portalWidth);
61
+ if (tmpWidth !== portalWidth) setWidth(portalWidth);
62
+
63
+ const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => {
64
+ if (!size) return;
65
+ const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width;
66
+ if (!nextWidth) return;
67
+
68
+ if (isEqual(nextWidth, portalWidth)) return;
69
+ setWidth(nextWidth);
70
+ updateSystemStatus({ portalWidth: nextWidth });
71
+ };
72
+
52
73
  return (
53
74
  showInspector && (
54
75
  <DraggablePanel
@@ -56,6 +77,7 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => {
56
77
  classNames={{
57
78
  content: styles.content,
58
79
  }}
80
+ defaultSize={{ width: tmpWidth }}
59
81
  expand
60
82
  hanlderStyle={{ display: 'none' }}
61
83
  maxWidth={CHAT_PORTAL_MAX_WIDTH}
@@ -63,9 +85,11 @@ const PortalPanel = memo(({ children }: PropsWithChildren) => {
63
85
  showArtifactUI || showToolUI || showThread ? CHAT_PORTAL_TOOL_UI_WIDTH : CHAT_PORTAL_WIDTH
64
86
  }
65
87
  mode={md ? 'fixed' : 'float'}
88
+ onSizeChange={handleSizeChange}
66
89
  placement={'right'}
67
90
  showHandlerWhenUnexpand={false}
68
91
  showHandlerWideArea={false}
92
+ size={{ height: '100%', width: portalWidth }}
69
93
  >
70
94
  <DraggablePanelContainer
71
95
  style={{
@@ -3,7 +3,7 @@ import { createStyles } from 'antd-style';
3
3
  import { AnimatePresence, motion } from 'framer-motion';
4
4
  import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
5
5
  import { rgba } from 'polished';
6
- import { memo, useEffect, useState } from 'react';
6
+ import { CSSProperties, memo, useEffect, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
@@ -61,30 +61,26 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
61
61
  interface ThinkingProps {
62
62
  content?: string;
63
63
  duration?: number;
64
+ style?: CSSProperties;
64
65
  thinking?: boolean;
65
66
  }
66
67
 
67
- const Thinking = memo<ThinkingProps>(({ content, duration, thinking }) => {
68
+ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style }) => {
68
69
  const { t } = useTranslation(['components', 'common']);
69
70
  const { styles, cx } = useStyles();
70
71
 
71
72
  const [showDetail, setShowDetail] = useState(false);
72
73
 
73
74
  useEffect(() => {
74
- if (thinking && !content) {
75
- setShowDetail(true);
76
- }
77
-
78
- if (!thinking) {
79
- setShowDetail(false);
80
- }
81
- }, [thinking, content]);
75
+ setShowDetail(!!thinking);
76
+ }, [thinking]);
82
77
 
83
78
  return (
84
- <Flexbox className={cx(styles.container, showDetail && styles.expand)} gap={16}>
79
+ <Flexbox className={cx(styles.container, showDetail && styles.expand)} gap={16} style={style}>
85
80
  <Flexbox
86
81
  distribution={'space-between'}
87
82
  flex={1}
83
+ gap={8}
88
84
  horizontal
89
85
  onClick={() => {
90
86
  setShowDetail(!showDetail);
@@ -92,18 +88,20 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking }) => {
92
88
  style={{ cursor: 'pointer' }}
93
89
  >
94
90
  {thinking ? (
95
- <Flexbox gap={8} horizontal>
91
+ <Flexbox align={'center'} gap={8} horizontal>
96
92
  <Icon icon={AtomIcon} />
97
93
  <Flexbox className={styles.shinyText} horizontal>
98
94
  {t('Thinking.thinking')}
99
95
  </Flexbox>
100
96
  </Flexbox>
101
97
  ) : (
102
- <Flexbox gap={8} horizontal>
98
+ <Flexbox align={'center'} gap={8} horizontal>
103
99
  <Icon icon={AtomIcon} />
104
- {!duration
105
- ? t('Thinking.thoughtWithDuration')
106
- : t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })}
100
+ <Flexbox>
101
+ {!duration
102
+ ? t('Thinking.thoughtWithDuration')
103
+ : t('Thinking.thought', { duration: ((duration || 0) / 1000).toFixed(1) })}
104
+ </Flexbox>
107
105
  </Flexbox>
108
106
  )}
109
107
  <Flexbox gap={4} horizontal>
@@ -1,6 +1,6 @@
1
1
  import { ModelProviderCard } from '@/types/llm';
2
2
 
3
- // ref :https://siliconflow.cn/zh-cn/pricing
3
+ // ref: https://siliconflow.cn/zh-cn/pricing
4
4
  const SiliconCloud: ModelProviderCard = {
5
5
  chatModels: [
6
6
  {
@@ -582,7 +582,7 @@ const SiliconCloud: ModelProviderCard = {
582
582
  vision: true,
583
583
  },
584
584
  ],
585
- checkModel: 'Qwen/Qwen2.5-7B-Instruct',
585
+ checkModel: 'Pro/Qwen/Qwen2-1.5B-Instruct',
586
586
  description: 'SiliconCloud,基于优秀开源基础模型的高性价比 GenAI 云服务',
587
587
  id: 'siliconcloud',
588
588
  modelList: { showModelFetcher: true },
@@ -1,14 +1,9 @@
1
- import { Icon } from '@lobehub/ui';
2
- import { createStyles } from 'antd-style';
3
- import { BringToFrontIcon, ChevronDown, ChevronRight, Loader2Icon } from 'lucide-react';
4
- import { memo, useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import { Flexbox } from 'react-layout-kit';
1
+ import { memo } from 'react';
7
2
 
3
+ import Thinking from '@/components/Thinking';
8
4
  import { ARTIFACT_THINKING_TAG } from '@/const/plugin';
9
5
  import { useChatStore } from '@/store/chat';
10
6
  import { chatSelectors } from '@/store/chat/selectors';
11
- import { dotLoading } from '@/styles/loading';
12
7
 
13
8
  import { MarkdownElementProps } from '../type';
14
9
 
@@ -22,64 +17,18 @@ export const isLobeThinkingClosed = (input: string = '') => {
22
17
  return input.includes(openTag) && input.includes(closeTag);
23
18
  };
24
19
 
25
- const useStyles = createStyles(({ css, token }) => ({
26
- container: css`
27
- cursor: pointer;
28
-
29
- padding-block: 8px;
30
- padding-inline: 12px;
31
- padding-inline-end: 12px;
32
- border-radius: 8px;
33
-
34
- color: ${token.colorText};
35
-
36
- background: ${token.colorFillQuaternary};
37
- `,
38
- title: css`
39
- overflow: hidden;
40
- display: -webkit-box;
41
- -webkit-box-orient: vertical;
42
- -webkit-line-clamp: 1;
43
-
44
- font-size: 12px;
45
- text-overflow: ellipsis;
46
- `,
47
- }));
48
-
49
20
  const Render = memo<MarkdownElementProps>(({ children, id }) => {
50
- const { t } = useTranslation('chat');
51
- const { styles, cx } = useStyles();
52
-
53
21
  const [isGenerating] = useChatStore((s) => {
54
22
  const message = chatSelectors.getMessageById(id)(s);
55
23
  return [!isLobeThinkingClosed(message?.content)];
56
24
  });
57
25
 
58
- const [showDetail, setShowDetail] = useState(false);
59
-
60
- const expand = showDetail || isGenerating;
61
26
  return (
62
- <Flexbox
63
- className={styles.container}
64
- gap={16}
65
- onClick={() => {
66
- setShowDetail(!showDetail);
67
- }}
68
- width={'100%'}
69
- >
70
- <Flexbox distribution={'space-between'} flex={1} horizontal>
71
- <Flexbox gap={8} horizontal>
72
- <Icon icon={isGenerating ? Loader2Icon : BringToFrontIcon} spin={isGenerating} />
73
- {isGenerating ? (
74
- <span className={cx(dotLoading)}>{t('artifact.thinking')}</span>
75
- ) : (
76
- t('artifact.thought')
77
- )}
78
- </Flexbox>
79
- <Icon icon={expand ? ChevronDown : ChevronRight} />
80
- </Flexbox>
81
- {expand && children}
82
- </Flexbox>
27
+ <Thinking
28
+ content={children as string}
29
+ style={{ width: isGenerating ? '100%' : undefined }}
30
+ thinking={isGenerating}
31
+ />
83
32
  );
84
33
  });
85
34
 
@@ -1,4 +1,5 @@
1
- import { ModelProvider } from '../types';
1
+ import { AgentRuntimeErrorType } from '../error';
2
+ import { ChatCompletionErrorPayload, ModelProvider } from '../types';
2
3
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
4
 
4
5
  import { LOBE_DEFAULT_MODEL_LIST } from '@/config/aiModels';
@@ -10,6 +11,33 @@ export interface SiliconCloudModelCard {
10
11
  export const LobeSiliconCloudAI = LobeOpenAICompatibleFactory({
11
12
  baseURL: 'https://api.siliconflow.cn/v1',
12
13
  chatCompletion: {
14
+ handleError: (error: any): Omit<ChatCompletionErrorPayload, 'provider'> | undefined => {
15
+ let errorResponse: Response | undefined;
16
+ if (error instanceof Response) {
17
+ errorResponse = error;
18
+ } else if ('status' in (error as any)) {
19
+ errorResponse = error as Response;
20
+ }
21
+ if (errorResponse) {
22
+ if (errorResponse.status === 401) {
23
+ return {
24
+ error: errorResponse.status,
25
+ errorType: AgentRuntimeErrorType.InvalidProviderAPIKey,
26
+ };
27
+ }
28
+
29
+ if (errorResponse.status === 403) {
30
+ return {
31
+ error: errorResponse.status,
32
+ errorType: AgentRuntimeErrorType.ProviderBizError,
33
+ message: '请检查 API Key 余额是否充足,或者是否在用未实名的 API Key 访问需要实名的模型。',
34
+ };
35
+ }
36
+ }
37
+ return {
38
+ error,
39
+ };
40
+ },
13
41
  handlePayload: (payload) => {
14
42
  return {
15
43
  ...payload,
@@ -20,6 +48,10 @@ export const LobeSiliconCloudAI = LobeOpenAICompatibleFactory({
20
48
  debug: {
21
49
  chatCompletion: () => process.env.DEBUG_SILICONCLOUD_CHAT_COMPLETION === '1',
22
50
  },
51
+ errorType: {
52
+ bizError: AgentRuntimeErrorType.ProviderBizError,
53
+ invalidAPIKey: AgentRuntimeErrorType.InvalidProviderAPIKey,
54
+ },
23
55
  models: {
24
56
  transformModel: (m) => {
25
57
  const functionCallKeywords = [
@@ -576,7 +576,7 @@ describe('chatMessage actions', () => {
576
576
  const abortController = new AbortController();
577
577
 
578
578
  act(() => {
579
- useChatStore.setState({ abortController });
579
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
580
580
  });
581
581
 
582
582
  await act(async () => {
@@ -596,18 +596,18 @@ describe('chatMessage actions', () => {
596
596
 
597
597
  await act(async () => {
598
598
  // 确保没有设置 abortController
599
- useChatStore.setState({ abortController: undefined });
599
+ useChatStore.setState({ chatLoadingIdsAbortController: undefined });
600
600
 
601
601
  result.current.stopGenerateMessage();
602
602
  });
603
603
 
604
604
  // 由于没有 abortController,不应调用任何方法
605
- expect(result.current.abortController).toBeUndefined();
605
+ expect(result.current.chatLoadingIdsAbortController).toBeUndefined();
606
606
  });
607
607
 
608
608
  it('should return early if abortController is undefined', () => {
609
609
  act(() => {
610
- useChatStore.setState({ abortController: undefined });
610
+ useChatStore.setState({ chatLoadingIdsAbortController: undefined });
611
611
  });
612
612
 
613
613
  const { result } = renderHook(() => useChatStore());
@@ -625,7 +625,7 @@ describe('chatMessage actions', () => {
625
625
  const abortMock = vi.fn();
626
626
  const abortController = { abort: abortMock } as unknown as AbortController;
627
627
  act(() => {
628
- useChatStore.setState({ abortController });
628
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
629
629
  });
630
630
  const { result } = renderHook(() => useChatStore());
631
631
 
@@ -639,7 +639,7 @@ describe('chatMessage actions', () => {
639
639
  it('should call internal_toggleChatLoading with correct parameters', () => {
640
640
  const abortController = new AbortController();
641
641
  act(() => {
642
- useChatStore.setState({ abortController });
642
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
643
643
  });
644
644
  const { result } = renderHook(() => useChatStore());
645
645
  const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
@@ -868,7 +868,7 @@ describe('chatMessage actions', () => {
868
868
  });
869
869
 
870
870
  const state = useChatStore.getState();
871
- expect(state.abortController).toBeInstanceOf(AbortController);
871
+ expect(state.chatLoadingIdsAbortController).toBeInstanceOf(AbortController);
872
872
  expect(state.chatLoadingIds).toEqual(['message-id']);
873
873
  });
874
874
 
@@ -887,7 +887,7 @@ describe('chatMessage actions', () => {
887
887
  });
888
888
 
889
889
  const state = useChatStore.getState();
890
- expect(state.abortController).toBeUndefined();
890
+ expect(state.chatLoadingIdsAbortController).toBeUndefined();
891
891
  expect(state.chatLoadingIds).toEqual([]);
892
892
  });
893
893
 
@@ -920,12 +920,12 @@ describe('chatMessage actions', () => {
920
920
  const abortController = new AbortController();
921
921
 
922
922
  act(() => {
923
- useChatStore.setState({ abortController });
923
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
924
924
  result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
925
925
  });
926
926
 
927
927
  const state = useChatStore.getState();
928
- expect(state.abortController).toEqual(abortController);
928
+ expect(state.chatLoadingIdsAbortController).toEqual(abortController);
929
929
  });
930
930
  });
931
931
 
@@ -267,10 +267,11 @@ export const generateAIChat: StateCreator<
267
267
  await Promise.all([summaryTitle(), addFilesToAgent()]);
268
268
  },
269
269
  stopGenerateMessage: () => {
270
- const { abortController, internal_toggleChatLoading } = get();
271
- if (!abortController) return;
270
+ const { chatLoadingIdsAbortController, internal_toggleChatLoading } = get();
272
271
 
273
- abortController.abort(MESSAGE_CANCEL_FLAT);
272
+ if (!chatLoadingIdsAbortController) return;
273
+
274
+ chatLoadingIdsAbortController.abort(MESSAGE_CANCEL_FLAT);
274
275
 
275
276
  internal_toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
276
277
  },
@@ -1,9 +1,9 @@
1
1
  export interface ChatAIChatState {
2
- abortController?: AbortController;
3
2
  /**
4
3
  * is the AI message is generating
5
4
  */
6
5
  chatLoadingIds: string[];
6
+ chatLoadingIdsAbortController?: AbortController;
7
7
  inputFiles: File[];
8
8
  inputMessage: string;
9
9
  /**
@@ -368,13 +368,14 @@ export const chatMessage: StateCreator<
368
368
  );
369
369
  },
370
370
  internal_toggleLoadingArrays: (key, loading, id, action) => {
371
+ const abortControllerKey = `${key}AbortController`;
371
372
  if (loading) {
372
373
  window.addEventListener('beforeunload', preventLeavingFn);
373
374
 
374
375
  const abortController = new AbortController();
375
376
  set(
376
377
  {
377
- abortController,
378
+ [abortControllerKey]: abortController,
378
379
  [key]: toggleBooleanList(get()[key] as string[], id!, loading),
379
380
  },
380
381
  false,
@@ -384,11 +385,11 @@ export const chatMessage: StateCreator<
384
385
  return abortController;
385
386
  } else {
386
387
  if (!id) {
387
- set({ abortController: undefined, [key]: [] }, false, action);
388
+ set({ [abortControllerKey]: undefined, [key]: [] }, false, action);
388
389
  } else
389
390
  set(
390
391
  {
391
- abortController: undefined,
392
+ [abortControllerKey]: undefined,
392
393
  [key]: toggleBooleanList(get()[key] as string[], id, loading),
393
394
  },
394
395
  false,
@@ -52,6 +52,7 @@ export interface SystemStatus {
52
52
  latestChangelogId?: string;
53
53
  mobileShowPortal?: boolean;
54
54
  mobileShowTopic?: boolean;
55
+ portalWidth: number;
55
56
  sessionsWidth: number;
56
57
  showChatSideBar?: boolean;
57
58
  showFilePanel?: boolean;
@@ -86,6 +87,7 @@ export const INITIAL_STATUS = {
86
87
  hideThreadLimitAlert: false,
87
88
  inputHeight: 200,
88
89
  mobileShowTopic: false,
90
+ portalWidth: 400,
89
91
  sessionsWidth: 320,
90
92
  showChatSideBar: true,
91
93
  showFilePanel: true,
@@ -20,6 +20,7 @@ const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
20
20
  const showChatHeader = (s: GlobalStore) => !s.status.zenMode;
21
21
  const inZenMode = (s: GlobalStore) => s.status.zenMode;
22
22
  const sessionWidth = (s: GlobalStore) => s.status.sessionsWidth;
23
+ const portalWidth = (s: GlobalStore) => s.status.portalWidth || 400;
23
24
  const filePanelWidth = (s: GlobalStore) => s.status.filePanelWidth;
24
25
  const inputHeight = (s: GlobalStore) => s.status.inputHeight;
25
26
  const threadInputHeight = (s: GlobalStore) => s.status.threadInputHeight;
@@ -59,6 +60,7 @@ export const systemStatusSelectors = {
59
60
  isPgliteNotInited,
60
61
  mobileShowPortal,
61
62
  mobileShowTopic,
63
+ portalWidth,
62
64
  sessionGroupKeys,
63
65
  sessionWidth,
64
66
  showChatHeader,