@lobehub/chat 0.155.5 → 0.155.6

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.
Files changed (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -1
  4. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +6 -6
  5. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +2 -1
  6. package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +2 -1
  7. package/src/components/ModelTag/ModelIcon.tsx +2 -2
  8. package/src/features/ChatInput/STT/browser.tsx +2 -1
  9. package/src/features/ChatInput/STT/openai.tsx +2 -1
  10. package/src/features/ChatInput/useChatInput.ts +2 -1
  11. package/src/features/ChatInput/useSend.ts +2 -1
  12. package/src/features/Conversation/Actions/customAction.ts +9 -0
  13. package/src/features/Conversation/Extras/Assistant.tsx +2 -1
  14. package/src/features/Conversation/Extras/User.tsx +2 -1
  15. package/src/features/Conversation/components/AutoScroll.tsx +1 -1
  16. package/src/features/Conversation/components/ChatItem/index.tsx +22 -12
  17. package/src/libs/agent-runtime/openrouter/index.ts +4 -1
  18. package/src/store/chat/slices/message/action.test.ts +52 -2
  19. package/src/store/chat/slices/message/action.ts +40 -13
  20. package/src/store/chat/slices/message/initialState.ts +13 -1
  21. package/src/store/chat/slices/message/selectors.test.ts +1 -1
  22. package/src/store/chat/slices/message/selectors.ts +10 -1
  23. package/src/store/chat/slices/plugin/action.test.ts +10 -2
  24. package/src/store/chat/slices/plugin/action.ts +6 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.155.6](https://github.com/lobehub/lobe-chat/compare/v0.155.5...v0.155.6)
6
+
7
+ <sup>Released on **2024-05-08**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix editing long message issue.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix editing long message issue, closes [#2431](https://github.com/lobehub/lobe-chat/issues/2431) ([380d8da](https://github.com/lobehub/lobe-chat/commit/380d8da))
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 0.155.5](https://github.com/lobehub/lobe-chat/compare/v0.155.4...v0.155.5)
6
31
 
7
32
  <sup>Released on **2024-05-08**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.155.5",
3
+ "version": "0.155.6",
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",
@@ -13,6 +13,7 @@ import { useSendMessage } from '@/features/ChatInput/useSend';
13
13
  import { useAgentStore } from '@/store/agent';
14
14
  import { agentSelectors } from '@/store/agent/slices/chat';
15
15
  import { useChatStore } from '@/store/chat';
16
+ import { chatSelectors } from '@/store/chat/selectors';
16
17
  import { useUserStore } from '@/store/user';
17
18
  import { modelProviderSelectors, preferenceSelectors } from '@/store/user/selectors';
18
19
  import { isMacOS } from '@/utils/platform';
@@ -60,7 +61,7 @@ const Footer = memo<FooterProps>(({ setExpand }) => {
60
61
  const { theme, styles } = useStyles();
61
62
 
62
63
  const [loading, stopGenerateMessage] = useChatStore((s) => [
63
- !!s.chatLoadingId,
64
+ chatSelectors.isAIGenerating(s),
64
65
  s.stopGenerateMessage,
65
66
  ]);
66
67
 
@@ -195,7 +195,7 @@ describe('<InputArea />', () => {
195
195
  it('does not send message when loading or shift key is pressed', () => {
196
196
  const sendMessageMock = vi.fn();
197
197
  act(() => {
198
- useChatStore.setState({ chatLoadingId: '123', sendMessage: sendMessageMock });
198
+ useChatStore.setState({ chatLoadingIds: ['123'], sendMessage: sendMessageMock });
199
199
  });
200
200
 
201
201
  render(<InputArea setExpand={setExpandMock} />);
@@ -209,7 +209,7 @@ describe('<InputArea />', () => {
209
209
  const sendMessageMock = vi.fn();
210
210
  act(() => {
211
211
  useChatStore.setState({
212
- chatLoadingId: '',
212
+ chatLoadingIds: [],
213
213
  inputMessage: 'abc',
214
214
  sendMessage: sendMessageMock,
215
215
  });
@@ -228,7 +228,7 @@ describe('<InputArea />', () => {
228
228
  const sendMessageMock = vi.fn();
229
229
  act(() => {
230
230
  useChatStore.setState({
231
- chatLoadingId: '',
231
+ chatLoadingIds: [],
232
232
  inputMessage: '123',
233
233
  sendMessage: sendMessageMock,
234
234
  });
@@ -247,7 +247,7 @@ describe('<InputArea />', () => {
247
247
  const updateInputMessageMock = vi.fn();
248
248
  act(() => {
249
249
  useChatStore.setState({
250
- chatLoadingId: '',
250
+ chatLoadingIds: [],
251
251
  inputMessage: 'Test',
252
252
  sendMessage: sendMessageMock,
253
253
  updateInputMessage: updateInputMessageMock,
@@ -271,7 +271,7 @@ describe('<InputArea />', () => {
271
271
  const sendMessageMock = vi.fn();
272
272
  act(() => {
273
273
  useChatStore.setState({
274
- chatLoadingId: '',
274
+ chatLoadingIds: [],
275
275
  inputMessage: '123',
276
276
  sendMessage: sendMessageMock,
277
277
  });
@@ -295,7 +295,7 @@ describe('<InputArea />', () => {
295
295
  const updateInputMessageMock = vi.fn();
296
296
  act(() => {
297
297
  useChatStore.setState({
298
- chatLoadingId: '',
298
+ chatLoadingIds: [],
299
299
  inputMessage: 'Test',
300
300
  sendMessage: sendMessageMock,
301
301
  updateInputMessage: updateInputMessageMock,
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useSendMessage } from '@/features/ChatInput/useSend';
8
8
  import { useChatStore } from '@/store/chat';
9
+ import { chatSelectors } from '@/store/chat/selectors';
9
10
  import { useUserStore } from '@/store/user';
10
11
  import { preferenceSelectors } from '@/store/user/selectors';
11
12
  import { isCommandPressed } from '@/utils/keyboard';
@@ -42,7 +43,7 @@ const InputArea = memo<InputAreaProps>(({ setExpand }) => {
42
43
  const isChineseInput = useRef(false);
43
44
 
44
45
  const [loading, value, updateInputMessage] = useChatStore((s) => [
45
- !!s.chatLoadingId,
46
+ chatSelectors.isAIGenerating(s),
46
47
  s.inputMessage,
47
48
  s.updateInputMessage,
48
49
  ]);
@@ -5,6 +5,7 @@ import { shallow } from 'zustand/shallow';
5
5
  import ModelTag from '@/components/ModelTag';
6
6
  import { useAgentStore } from '@/store/agent';
7
7
  import { useChatStore } from '@/store/chat';
8
+ import { chatSelectors } from '@/store/chat/selectors';
8
9
  import { useSessionStore } from '@/store/session';
9
10
  import { sessionHelpers } from '@/store/session/helpers';
10
11
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
@@ -23,7 +24,7 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
23
24
  const [defaultModel] = useAgentStore((s) => [s.defaultAgentConfig.model]);
24
25
 
25
26
  const [active] = useSessionStore((s) => [s.activeId === id]);
26
- const [loading] = useChatStore((s) => [!!s.chatLoadingId && id === s.activeId]);
27
+ const [loading] = useChatStore((s) => [chatSelectors.isAIGenerating(s) && id === s.activeId]);
27
28
 
28
29
  const [pin, title, description, avatar, avatarBackground, updateAt, model, group] =
29
30
  useSessionStore((s) => {
@@ -27,7 +27,7 @@ import {
27
27
  Stability,
28
28
  Tongyi,
29
29
  Wenxin,
30
- ZeroOne,
30
+ Yi,
31
31
  } from '@lobehub/icons';
32
32
  import { memo } from 'react';
33
33
 
@@ -54,7 +54,7 @@ const ModelIcon = memo<ModelIconProps>(({ model, size = 12 }) => {
54
54
  if (model.includes('abab')) return <Minimax size={size} />;
55
55
  if (model.includes('mistral') || model.includes('mixtral')) return <Mistral size={size} />;
56
56
  if (model.includes('pplx') || model.includes('sonar')) return <Perplexity size={size} />;
57
- if (model.includes('yi-')) return <ZeroOne size={size} />;
57
+ if (model.includes('yi-')) return <Yi size={size} />;
58
58
  if (model.startsWith('openrouter')) return <OpenRouter size={size} />; // only for Cinematika and Auto
59
59
  if (model.startsWith('openchat')) return <OpenChat size={size} />;
60
60
  if (model.includes('command')) return <Cohere size={size} />;
@@ -7,6 +7,7 @@ import { SWRConfiguration } from 'swr';
7
7
  import { useAgentStore } from '@/store/agent';
8
8
  import { agentSelectors } from '@/store/agent/slices/chat';
9
9
  import { useChatStore } from '@/store/chat';
10
+ import { chatSelectors } from '@/store/chat/selectors';
10
11
  import { useUserStore } from '@/store/user';
11
12
  import { settingsSelectors } from '@/store/user/selectors';
12
13
  import { ChatMessageError } from '@/types/message';
@@ -40,7 +41,7 @@ const BrowserSTT = memo<{ mobile?: boolean }>(({ mobile }) => {
40
41
  const { t } = useTranslation('chat');
41
42
 
42
43
  const [loading, updateInputMessage] = useChatStore((s) => [
43
- !!s.chatLoadingId,
44
+ chatSelectors.isAIGenerating(s),
44
45
  s.updateInputMessage,
45
46
  ]);
46
47
 
@@ -10,6 +10,7 @@ import { API_ENDPOINTS } from '@/services/_url';
10
10
  import { useAgentStore } from '@/store/agent';
11
11
  import { agentSelectors } from '@/store/agent/selectors';
12
12
  import { useChatStore } from '@/store/chat';
13
+ import { chatSelectors } from '@/store/chat/slices/message/selectors';
13
14
  import { useUserStore } from '@/store/user';
14
15
  import { settingsSelectors } from '@/store/user/selectors';
15
16
  import { ChatMessageError } from '@/types/message';
@@ -51,7 +52,7 @@ const OpenaiSTT = memo<{ mobile?: boolean }>(({ mobile }) => {
51
52
  const { t } = useTranslation('chat');
52
53
 
53
54
  const [loading, updateInputMessage] = useChatStore((s) => [
54
- !!s.chatLoadingId,
55
+ chatSelectors.isAIGenerating(s),
55
56
  s.updateInputMessage,
56
57
  ]);
57
58
 
@@ -4,6 +4,7 @@ import { useCallback, useRef, useState } from 'react';
4
4
  import { useAgentStore } from '@/store/agent';
5
5
  import { agentSelectors } from '@/store/agent/slices/chat';
6
6
  import { useChatStore } from '@/store/chat';
7
+ import { chatSelectors } from '@/store/chat/selectors';
7
8
  import { useUserStore } from '@/store/user';
8
9
  import { modelProviderSelectors } from '@/store/user/selectors';
9
10
 
@@ -18,7 +19,7 @@ export const useChatInput = () => {
18
19
  const canUpload = useUserStore(modelProviderSelectors.isModelEnabledUpload(model));
19
20
 
20
21
  const [loading, value, onInput, onStop] = useChatStore((s) => [
21
- !!s.chatLoadingId,
22
+ chatSelectors.isAIGenerating(s),
22
23
  s.inputMessage,
23
24
  s.updateInputMessage,
24
25
  s.stopGenerateMessage,
@@ -1,6 +1,7 @@
1
1
  import { useCallback } from 'react';
2
2
 
3
3
  import { useChatStore } from '@/store/chat';
4
+ import { chatSelectors } from '@/store/chat/selectors';
4
5
  import { SendMessageParams } from '@/store/chat/slices/message/action';
5
6
  import { filesSelectors, useFileStore } from '@/store/file';
6
7
 
@@ -17,7 +18,7 @@ export const useSendMessage = () => {
17
18
 
18
19
  return useCallback((params: UseSendMessageParams = {}) => {
19
20
  const store = useChatStore.getState();
20
- if (!!store.chatLoadingId) return;
21
+ if (chatSelectors.isAIGenerating(store)) return;
21
22
  if (!store.inputMessage) return;
22
23
 
23
24
  const imageList = filesSelectors.imageUrlOrBase64List(useFileStore.getState());
@@ -1,10 +1,18 @@
1
1
  import { ActionIconGroupItems } from '@lobehub/ui/es/ActionIconGroup';
2
+ import { css, cx } from 'antd-style';
2
3
  import { LanguagesIcon, Play } from 'lucide-react';
3
4
  import { useMemo } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
6
 
6
7
  import { localeOptions } from '@/locales/resources';
7
8
 
9
+ const translateStyle = css`
10
+ .ant-dropdown-menu-sub {
11
+ overflow-y: scroll;
12
+ max-height: 400px;
13
+ }
14
+ `;
15
+
8
16
  export const useCustomActions = () => {
9
17
  const { t } = useTranslation('chat');
10
18
 
@@ -16,6 +24,7 @@ export const useCustomActions = () => {
16
24
  icon: LanguagesIcon,
17
25
  key: 'translate',
18
26
  label: t('translate.action'),
27
+ popupClassName: cx(translateStyle),
19
28
  } as ActionIconGroupItems;
20
29
 
21
30
  const tts = {
@@ -5,6 +5,7 @@ import ModelTag from '@/components/ModelTag';
5
5
  import { useAgentStore } from '@/store/agent';
6
6
  import { agentSelectors } from '@/store/agent/slices/chat';
7
7
  import { useChatStore } from '@/store/chat';
8
+ import { chatSelectors } from '@/store/chat/selectors';
8
9
  import { ChatMessage } from '@/types/message';
9
10
 
10
11
  import { RenderMessageExtra } from '../types';
@@ -15,7 +16,7 @@ import Translate from './Translate';
15
16
  export const AssistantMessageExtra: RenderMessageExtra = memo<ChatMessage>(
16
17
  ({ extra, id, content }) => {
17
18
  const model = useAgentStore(agentSelectors.currentAgentModel);
18
- const loading = useChatStore((s) => s.chatLoadingId === id);
19
+ const loading = useChatStore(chatSelectors.isMessageGenerating(id));
19
20
 
20
21
  const showModelTag = extra?.fromModel && model !== extra?.fromModel;
21
22
  const showTranslate = !!extra?.translate;
@@ -1,6 +1,7 @@
1
1
  import { memo } from 'react';
2
2
 
3
3
  import { useChatStore } from '@/store/chat';
4
+ import { chatSelectors } from '@/store/chat/selectors';
4
5
  import { ChatMessage } from '@/types/message';
5
6
 
6
7
  import { RenderMessageExtra } from '../types';
@@ -9,7 +10,7 @@ import TTS from './TTS';
9
10
  import Translate from './Translate';
10
11
 
11
12
  export const UserMessageExtra: RenderMessageExtra = memo<ChatMessage>(({ extra, id, content }) => {
12
- const loading = useChatStore((s) => s.chatLoadingId === id);
13
+ const loading = useChatStore(chatSelectors.isMessageGenerating(id));
13
14
 
14
15
  const showTranslate = !!extra?.translate;
15
16
  const showTTS = !!extra?.tts;
@@ -11,7 +11,7 @@ interface AutoScrollProps {
11
11
  onScrollToBottom: (type: 'auto' | 'click') => void;
12
12
  }
13
13
  const AutoScroll = memo<AutoScrollProps>(({ atBottom, isScrolling, onScrollToBottom }) => {
14
- const trackVisibility = useChatStore((s) => !!s.chatLoadingId);
14
+ const trackVisibility = useChatStore(chatSelectors.isAIGenerating);
15
15
  const str = useChatStore(chatSelectors.chatsMessageString);
16
16
 
17
17
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  import { AlertProps, ChatItem } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
3
  import isEqual from 'fast-deep-equal';
4
- import { ReactNode, memo, useCallback, useMemo, useState } from 'react';
4
+ import { ReactNode, memo, useCallback, useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useAgentStore } from '@/store/agent';
@@ -41,7 +41,6 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
41
41
  const fontSize = useUserStore((s) => settingsSelectors.currentSettings(s).fontSize);
42
42
  const { t } = useTranslation('common');
43
43
  const { styles, cx } = useStyles();
44
- const [editing, setEditing] = useState(false);
45
44
  const [type = 'chat'] = useAgentStore((s) => {
46
45
  const config = agentSelectors.currentAgentConfig(s);
47
46
  return [config.displayMode];
@@ -58,12 +57,14 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
58
57
 
59
58
  const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
60
59
 
61
- const [loading, updateMessageContent] = useChatStore((s) => [
62
- s.chatLoadingId === id || s.messageLoadingIds.includes(id),
63
- s.modifyMessageContent,
64
- ]);
65
-
66
- const [isMessageLoading] = useChatStore((s) => [s.messageLoadingIds.includes(id)]);
60
+ const [isMessageLoading, generating, editing, toggleMessageEditing, updateMessageContent] =
61
+ useChatStore((s) => [
62
+ chatSelectors.isMessageLoading(id)(s),
63
+ chatSelectors.isMessageGenerating(id)(s),
64
+ chatSelectors.isMessageEditing(id)(s),
65
+ s.toggleMessageEditing,
66
+ s.modifyMessageContent,
67
+ ]);
67
68
 
68
69
  const onAvatarsClick = useAvatarsClick();
69
70
 
@@ -115,14 +116,21 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
115
116
  <>
116
117
  <HistoryDivider enable={enableHistoryDivider} />
117
118
  <ChatItem
118
- actions={<ActionsBar index={index} setEditing={setEditing} />}
119
+ actions={
120
+ <ActionsBar
121
+ index={index}
122
+ setEditing={(edit) => {
123
+ toggleMessageEditing(id, edit);
124
+ }}
125
+ />
126
+ }
119
127
  avatar={item.meta}
120
128
  className={cx(styles.message, isMessageLoading && styles.loading)}
121
129
  editing={editing}
122
130
  error={error}
123
131
  errorMessage={<ErrorMessageExtra data={item} />}
124
132
  fontSize={fontSize}
125
- loading={loading}
133
+ loading={generating}
126
134
  message={item.content}
127
135
  messageExtra={<MessageExtra data={item} />}
128
136
  onAvatarClick={onAvatarsClick?.(item.role)}
@@ -130,10 +138,12 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
130
138
  onDoubleClick={(e) => {
131
139
  if (item.id === 'default' || item.error) return;
132
140
  if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
133
- setEditing(true);
141
+ toggleMessageEditing(id, true);
134
142
  }
135
143
  }}
136
- onEditingChange={setEditing}
144
+ onEditingChange={(edit) => {
145
+ toggleMessageEditing(id, edit);
146
+ }}
137
147
  placement={type === 'chat' ? (item.role === 'user' ? 'right' : 'left') : 'left'}
138
148
  primary={item.role === 'user'}
139
149
  renderMessage={(editableContent) => (
@@ -36,7 +36,10 @@ export const LobeOpenRouterAI = LobeOpenAICompatibleFactory({
36
36
  ? model.top_provider.max_completion_tokens
37
37
  : undefined,
38
38
  tokens: model.context_length,
39
- vision: model.description.includes('vision') || model.id.includes('vision'),
39
+ vision:
40
+ model.description.includes('vision') ||
41
+ model.description.includes('multimodal') ||
42
+ model.id.includes('vision'),
40
43
  };
41
44
  },
42
45
  },
@@ -365,6 +365,31 @@ describe('chatMessage actions', () => {
365
365
  });
366
366
  });
367
367
 
368
+ describe('toggleMessageEditing action', () => {
369
+ it('should add message id to messageEditingIds when editing is true', () => {
370
+ const { result } = renderHook(() => useChatStore());
371
+ const messageId = 'message-id';
372
+
373
+ act(() => {
374
+ result.current.toggleMessageEditing(messageId, true);
375
+ });
376
+
377
+ expect(result.current.messageEditingIds).toContain(messageId);
378
+ });
379
+
380
+ it('should remove message id from messageEditingIds when editing is false', () => {
381
+ const { result } = renderHook(() => useChatStore());
382
+ const messageId = 'abc';
383
+
384
+ act(() => {
385
+ result.current.toggleMessageEditing(messageId, true);
386
+ result.current.toggleMessageEditing(messageId, false);
387
+ });
388
+
389
+ expect(result.current.messageEditingIds).not.toContain(messageId);
390
+ });
391
+ });
392
+
368
393
  describe('internal_resendMessage action', () => {
369
394
  it('should resend a message by id and refresh messages', async () => {
370
395
  const { result } = renderHook(() => useChatStore());
@@ -701,7 +726,7 @@ describe('chatMessage actions', () => {
701
726
 
702
727
  const state = useChatStore.getState();
703
728
  expect(state.abortController).toBeInstanceOf(AbortController);
704
- expect(state.chatLoadingId).toEqual('message-id');
729
+ expect(state.chatLoadingIds).toEqual(['message-id']);
705
730
  });
706
731
 
707
732
  it('should clear loading state and abort controller when loading is false', () => {
@@ -720,7 +745,7 @@ describe('chatMessage actions', () => {
720
745
 
721
746
  const state = useChatStore.getState();
722
747
  expect(state.abortController).toBeUndefined();
723
- expect(state.chatLoadingId).toBeUndefined();
748
+ expect(state.chatLoadingIds).toEqual([]);
724
749
  });
725
750
 
726
751
  it('should attach beforeunload event listener when loading starts', () => {
@@ -760,4 +785,29 @@ describe('chatMessage actions', () => {
760
785
  expect(state.abortController).toEqual(abortController);
761
786
  });
762
787
  });
788
+
789
+ describe('internal_toggleMessageLoading action', () => {
790
+ it('should add message id to messageLoadingIds when loading is true', () => {
791
+ const { result } = renderHook(() => useChatStore());
792
+ const messageId = 'message-id';
793
+
794
+ act(() => {
795
+ result.current.internal_toggleMessageLoading(true, messageId);
796
+ });
797
+
798
+ expect(result.current.messageLoadingIds).toContain(messageId);
799
+ });
800
+
801
+ it('should remove message id from messageLoadingIds when loading is false', () => {
802
+ const { result } = renderHook(() => useChatStore());
803
+ const messageId = 'ddd-id';
804
+
805
+ act(() => {
806
+ result.current.internal_toggleMessageLoading(true, messageId);
807
+ result.current.internal_toggleMessageLoading(false, messageId);
808
+ });
809
+
810
+ expect(result.current.messageLoadingIds).not.toContain(messageId);
811
+ });
812
+ });
763
813
  });
@@ -72,7 +72,7 @@ export interface ChatMessageAction {
72
72
  stopGenerateMessage: () => void;
73
73
  copyMessage: (id: string, content: string) => Promise<void>;
74
74
  refreshMessages: () => Promise<void>;
75
-
75
+ toggleMessageEditing: (id: string, editing: boolean) => void;
76
76
  // ========= ↓ Internal Method ↓ ========== //
77
77
  // ========================================== //
78
78
  // ========================================== //
@@ -137,6 +137,18 @@ const preventLeavingFn = (e: BeforeUnloadEvent) => {
137
137
  e.returnValue = '你有正在生成中的请求,确定要离开吗?';
138
138
  };
139
139
 
140
+ const toggleBooleanList = (ids: string[], id: string, loading: boolean) => {
141
+ return produce(ids, (draft) => {
142
+ if (loading) {
143
+ draft.push(id);
144
+ } else {
145
+ const index = draft.indexOf(id);
146
+
147
+ if (index >= 0) draft.splice(index, 1);
148
+ }
149
+ });
150
+ };
151
+
140
152
  export const chatMessage: StateCreator<
141
153
  ChatStore,
142
154
  [['zustand/devtools', never]],
@@ -244,7 +256,13 @@ export const chatMessage: StateCreator<
244
256
 
245
257
  get().internal_traceMessage(id, { eventType: TraceEventType.CopyMessage });
246
258
  },
247
-
259
+ toggleMessageEditing: (id, editing) => {
260
+ set(
261
+ { messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
262
+ false,
263
+ 'toggleMessageEditing',
264
+ );
265
+ },
248
266
  stopGenerateMessage: () => {
249
267
  const { abortController, internal_toggleChatLoading } = get();
250
268
  if (!abortController) return;
@@ -518,11 +536,28 @@ export const chatMessage: StateCreator<
518
536
  window.addEventListener('beforeunload', preventLeavingFn);
519
537
 
520
538
  const abortController = new AbortController();
521
- set({ abortController, chatLoadingId: id }, false, action);
539
+ set(
540
+ {
541
+ abortController,
542
+ chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id!, loading),
543
+ },
544
+ false,
545
+ action,
546
+ );
522
547
 
523
548
  return abortController;
524
549
  } else {
525
- set({ abortController: undefined, chatLoadingId: undefined }, false, action);
550
+ if (!id) {
551
+ set({ abortController: undefined, chatLoadingIds: [] }, false, action);
552
+ } else
553
+ set(
554
+ {
555
+ abortController: undefined,
556
+ chatLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
557
+ },
558
+ false,
559
+ action,
560
+ );
526
561
 
527
562
  window.removeEventListener('beforeunload', preventLeavingFn);
528
563
  }
@@ -530,15 +565,7 @@ export const chatMessage: StateCreator<
530
565
  internal_toggleMessageLoading: (loading, id) => {
531
566
  set(
532
567
  {
533
- messageLoadingIds: produce(get().messageLoadingIds, (draft) => {
534
- if (loading) {
535
- draft.push(id);
536
- } else {
537
- const index = draft.indexOf(id);
538
-
539
- if (index >= 0) draft.splice(index, 1);
540
- }
541
- }),
568
+ messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
542
569
  },
543
570
  false,
544
571
  'internal_toggleMessageLoading',
@@ -7,8 +7,18 @@ export interface ChatMessageState {
7
7
  * @description 当前正在编辑或查看的会话
8
8
  */
9
9
  activeId: string;
10
- chatLoadingId?: string;
10
+ /**
11
+ * is the AI message is generating
12
+ */
13
+ chatLoadingIds: string[];
11
14
  inputMessage: string;
15
+ /**
16
+ * is the message is editing
17
+ */
18
+ messageEditingIds: string[];
19
+ /**
20
+ * is the message is creating or updating in the service
21
+ */
12
22
  messageLoadingIds: string[];
13
23
  messages: ChatMessage[];
14
24
  /**
@@ -19,7 +29,9 @@ export interface ChatMessageState {
19
29
 
20
30
  export const initialMessageState: ChatMessageState = {
21
31
  activeId: 'inbox',
32
+ chatLoadingIds: [],
22
33
  inputMessage: '',
34
+ messageEditingIds: [],
23
35
  messageLoadingIds: [],
24
36
  messages: [],
25
37
  messagesInit: false,
@@ -107,7 +107,7 @@ describe('chatSelectors', () => {
107
107
  it('should return the properties of a function message', () => {
108
108
  const state = merge(initialStore, {
109
109
  messages: mockMessages,
110
- chatLoadingId: 'msg3', // Assuming this id represents a loading state
110
+ chatLoadingIds: ['msg3'], // Assuming this id represents a loading state
111
111
  });
112
112
  const props = chatSelectors.getFunctionMessageProps(mockMessages[2])(state);
113
113
  expect(props).toEqual({
@@ -123,7 +123,7 @@ const getFunctionMessageProps =
123
123
  command: plugin,
124
124
  content,
125
125
  id: plugin?.identifier,
126
- loading: id === s.chatLoadingId,
126
+ loading: s.chatLoadingIds.includes(id),
127
127
  type: plugin?.type as LobePluginType,
128
128
  });
129
129
 
@@ -134,6 +134,11 @@ const latestMessage = (s: ChatStore) => currentChats(s).at(-1);
134
134
 
135
135
  const currentChatLoadingState = (s: ChatStore) => !s.messagesInit;
136
136
 
137
+ const isMessageEditing = (id: string) => (s: ChatStore) => s.messageEditingIds.includes(id);
138
+ const isMessageLoading = (id: string) => (s: ChatStore) => s.messageLoadingIds.includes(id);
139
+ const isMessageGenerating = (id: string) => (s: ChatStore) => s.chatLoadingIds.includes(id);
140
+ const isAIGenerating = (s: ChatStore) => s.chatLoadingIds.length > 0;
141
+
137
142
  export const chatSelectors = {
138
143
  chatsMessageString,
139
144
  currentChatIDsWithGuideMessage,
@@ -145,6 +150,10 @@ export const chatSelectors = {
145
150
  getFunctionMessageProps,
146
151
  getMessageById,
147
152
  getTraceIdByMessageId,
153
+ isAIGenerating,
154
+ isMessageEditing,
155
+ isMessageGenerating,
156
+ isMessageLoading,
148
157
  latestMessage,
149
158
  showInboxWelcome,
150
159
  };
@@ -131,7 +131,11 @@ describe('ChatPluginAction', () => {
131
131
  });
132
132
  expect(storeState.refreshMessages).toHaveBeenCalled();
133
133
  expect(storeState.triggerAIMessage).toHaveBeenCalled();
134
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(false);
134
+ expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
135
+ false,
136
+ 'message-id',
137
+ 'plugin/fetchPlugin/end',
138
+ );
135
139
  });
136
140
 
137
141
  it('should handle errors when the plugin API call fails', async () => {
@@ -159,7 +163,11 @@ describe('ChatPluginAction', () => {
159
163
  expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { trace: {} });
160
164
  expect(messageService.updateMessageError).toHaveBeenCalledWith(messageId, error);
161
165
  expect(storeState.refreshMessages).toHaveBeenCalled();
162
- expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(false);
166
+ expect(storeState.internal_toggleChatLoading).toHaveBeenCalledWith(
167
+ false,
168
+ 'message-id',
169
+ 'plugin/fetchPlugin/end',
170
+ );
163
171
  expect(storeState.triggerAIMessage).not.toHaveBeenCalled(); // 确保在错误情况下不调用此方法
164
172
  });
165
173
  });
@@ -135,7 +135,11 @@ export const chatPlugin: StateCreator<
135
135
  let data: string;
136
136
 
137
137
  try {
138
- const abortController = internal_toggleChatLoading(true, id, n('fetchPlugin') as string);
138
+ const abortController = internal_toggleChatLoading(
139
+ true,
140
+ id,
141
+ n('fetchPlugin/start') as string,
142
+ );
139
143
 
140
144
  const message = chatSelectors.getMessageById(id)(get());
141
145
 
@@ -162,7 +166,7 @@ export const chatPlugin: StateCreator<
162
166
  data = '';
163
167
  }
164
168
 
165
- internal_toggleChatLoading(false);
169
+ internal_toggleChatLoading(false, id, n('fetchPlugin/end') as string);
166
170
  // 如果报错则结束了
167
171
  if (!data) return;
168
172