@lobehub/lobehub 2.0.0-next.64 → 2.0.0-next.66

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 (75) hide show
  1. package/.github/workflows/claude-translator.yml +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +18 -0
  4. package/locales/ar/chat.json +3 -0
  5. package/locales/ar/plugin.json +5 -0
  6. package/locales/bg-BG/chat.json +3 -0
  7. package/locales/bg-BG/plugin.json +5 -0
  8. package/locales/de-DE/chat.json +3 -0
  9. package/locales/de-DE/plugin.json +5 -0
  10. package/locales/en-US/chat.json +3 -0
  11. package/locales/es-ES/chat.json +3 -0
  12. package/locales/es-ES/plugin.json +5 -0
  13. package/locales/fa-IR/chat.json +3 -0
  14. package/locales/fa-IR/plugin.json +5 -0
  15. package/locales/fr-FR/chat.json +3 -0
  16. package/locales/fr-FR/plugin.json +5 -0
  17. package/locales/it-IT/chat.json +3 -0
  18. package/locales/it-IT/plugin.json +5 -0
  19. package/locales/ja-JP/chat.json +3 -0
  20. package/locales/ja-JP/plugin.json +5 -0
  21. package/locales/ko-KR/chat.json +3 -0
  22. package/locales/ko-KR/plugin.json +5 -0
  23. package/locales/nl-NL/chat.json +3 -0
  24. package/locales/nl-NL/plugin.json +5 -0
  25. package/locales/pl-PL/chat.json +3 -0
  26. package/locales/pl-PL/plugin.json +5 -0
  27. package/locales/pt-BR/chat.json +3 -0
  28. package/locales/pt-BR/plugin.json +5 -0
  29. package/locales/ru-RU/chat.json +3 -0
  30. package/locales/ru-RU/plugin.json +5 -0
  31. package/locales/tr-TR/chat.json +3 -0
  32. package/locales/tr-TR/plugin.json +5 -0
  33. package/locales/vi-VN/chat.json +3 -0
  34. package/locales/vi-VN/plugin.json +5 -0
  35. package/locales/zh-CN/chat.json +3 -0
  36. package/locales/zh-CN/plugin.json +2 -2
  37. package/locales/zh-TW/chat.json +3 -0
  38. package/locales/zh-TW/plugin.json +5 -0
  39. package/package.json +5 -5
  40. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +4 -8
  41. package/packages/conversation-flow/src/__tests__/fixtures/inputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +2 -1
  42. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistantGroup/index.ts +8 -0
  43. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +2 -4
  44. package/packages/conversation-flow/src/__tests__/fixtures/outputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +8 -8
  45. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistantGroup/index.ts +8 -0
  46. package/packages/conversation-flow/src/__tests__/parse.test.ts +6 -6
  47. package/packages/conversation-flow/src/parse.ts +45 -1
  48. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +64 -0
  49. package/packages/database/package.json +2 -2
  50. package/packages/obervability-otel/package.json +1 -1
  51. package/packages/types/src/message/common/metadata.ts +8 -1
  52. package/packages/types/src/message/ui/chat.ts +1 -0
  53. package/src/app/(backend)/market/agent/[[...segments]]/route.ts +1 -1
  54. package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +1 -1
  55. package/src/app/market-auth-callback/layout.tsx +27 -3
  56. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +15 -1
  58. package/src/features/Conversation/Messages/Assistant/CollapsedMessage.tsx +37 -0
  59. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +16 -9
  60. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +28 -6
  61. package/src/features/Conversation/Messages/Group/CollapsedMessage.tsx +37 -0
  62. package/src/features/Conversation/Messages/Group/{GroupChildren.tsx → Group.tsx} +18 -4
  63. package/src/features/Conversation/Messages/Group/index.tsx +4 -6
  64. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +14 -0
  65. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +1 -1
  66. package/src/libs/mcp/__tests__/index.test.ts +6 -6
  67. package/src/locales/default/chat.ts +3 -0
  68. package/src/store/chat/slices/message/actions/publicApi.ts +17 -0
  69. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -1
  70. package/src/store/chat/slices/message/selectors/messageState.ts +7 -0
  71. package/src/store/chat/slices/translate/action.test.ts +26 -32
  72. package/src/store/chat/slices/translate/action.ts +3 -3
  73. /package/packages/conversation-flow/src/__tests__/fixtures/inputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  74. /package/packages/conversation-flow/src/__tests__/fixtures/outputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  75. /package/src/features/Conversation/Messages/Group/{GroupContext.tsx → GroupContext.ts} +0 -0
@@ -3,6 +3,7 @@ import { UIChatMessage } from '@lobechat/types';
3
3
  import { ReactNode, memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
+ import { CollapsedMessage } from '@/features/Conversation/Messages/Assistant/CollapsedMessage';
6
7
  import { useChatStore } from '@/store/chat';
7
8
  import { aiChatSelectors, messageStateSelectors } from '@/store/chat/selectors';
8
9
 
@@ -18,9 +19,10 @@ export const AssistantMessageContent = memo<
18
19
  editableContent: ReactNode;
19
20
  }
20
21
  >(({ id, tools, content, chunksList, search, imageList, ...props }) => {
21
- const [editing, generating] = useChatStore((s) => [
22
+ const [editing, generating, isCollapsed] = useChatStore((s) => [
22
23
  messageStateSelectors.isMessageEditing(id)(s),
23
24
  messageStateSelectors.isMessageGenerating(id)(s),
25
+ messageStateSelectors.isMessageCollapsed(id)(s),
24
26
  ]);
25
27
 
26
28
  const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
@@ -40,14 +42,19 @@ export const AssistantMessageContent = memo<
40
42
 
41
43
  const showFileChunks = !!chunksList && chunksList.length > 0;
42
44
 
43
- return editing ? (
44
- <DefaultMessage
45
- content={content}
46
- id={id}
47
- isToolCallGenerating={isToolCallGenerating}
48
- {...props}
49
- />
50
- ) : (
45
+ if (editing)
46
+ return (
47
+ <DefaultMessage
48
+ content={content}
49
+ id={id}
50
+ isToolCallGenerating={isToolCallGenerating}
51
+ {...props}
52
+ />
53
+ );
54
+
55
+ if (isCollapsed) return <CollapsedMessage content={content} id={id} />;
56
+
57
+ return (
51
58
  <Flexbox gap={8} id={id}>
52
59
  {showSearch && (
53
60
  <SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
@@ -24,19 +24,30 @@ interface GroupActionsProps {
24
24
 
25
25
  const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }) => {
26
26
  const { tools } = data;
27
- const [isThreadMode, hasThread, isRegenerating] = useChatStore((s) => [
27
+ const [isThreadMode, hasThread, isRegenerating, isCollapsed] = useChatStore((s) => [
28
28
  !!s.activeThreadId,
29
29
  threadSelectors.hasThreadBySourceMsgId(id)(s),
30
30
  messageStateSelectors.isMessageRegenerating(id)(s),
31
+ messageStateSelectors.isMessageCollapsed(id)(s),
31
32
  ]);
32
33
  const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
33
34
  const [showShareModal, setShareModal] = useState(false);
34
35
 
35
- const { edit, delAndRegenerate, regenerate, copy, divider, del, branching, share } =
36
- useChatListActionsBar({
37
- hasThread,
38
- isRegenerating,
39
- });
36
+ const {
37
+ edit,
38
+ delAndRegenerate,
39
+ regenerate,
40
+ copy,
41
+ divider,
42
+ del,
43
+ branching,
44
+ share,
45
+ expand,
46
+ collapse,
47
+ } = useChatListActionsBar({
48
+ hasThread,
49
+ isRegenerating,
50
+ });
40
51
 
41
52
  const hasTools = !!tools;
42
53
 
@@ -64,6 +75,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
64
75
  resendThreadMessage,
65
76
  delAndResendThreadMessage,
66
77
  toggleMessageEditing,
78
+ toggleMessageCollapsed,
67
79
  ] = useChatStore((s) => [
68
80
  s.deleteMessage,
69
81
  s.regenerateAssistantMessage,
@@ -74,6 +86,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
74
86
  s.resendThreadMessage,
75
87
  s.delAndResendThreadMessage,
76
88
  s.toggleMessageEditing,
89
+ s.toggleMessageCollapsed,
77
90
  ]);
78
91
  const { message } = App.useApp();
79
92
  const virtuosoRef = use(VirtuosoContext);
@@ -133,6 +146,12 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
133
146
  setShareModal(true);
134
147
  break;
135
148
  }
149
+
150
+ case 'collapse':
151
+ case 'expand': {
152
+ toggleMessageCollapsed(id);
153
+ break;
154
+ }
136
155
  }
137
156
 
138
157
  if (action.keyPath.at(-1) === 'translate') {
@@ -146,6 +165,8 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
146
165
  [data, topic],
147
166
  );
148
167
 
168
+ const collapseAction = isCollapsed ? expand : collapse;
169
+
149
170
  return (
150
171
  <>
151
172
  <ActionIconGroup
@@ -154,6 +175,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
154
175
  items: [
155
176
  edit,
156
177
  copy,
178
+ collapseAction,
157
179
  divider,
158
180
  share,
159
181
  divider,
@@ -0,0 +1,37 @@
1
+ import { Button, Markdown, MaskShadow } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ interface CollapsedMessageProps {
9
+ content: string;
10
+ id: string;
11
+ }
12
+
13
+ export const CollapsedMessage = memo<CollapsedMessageProps>(({ id, content }) => {
14
+ const { t } = useTranslation('chat');
15
+ const toggleMessageCollapsed = useChatStore((s) => s.toggleMessageCollapsed);
16
+
17
+ return (
18
+ <Flexbox>
19
+ <MaskShadow>
20
+ <Markdown variant={'chat'}>{content?.slice(0, 300)}</Markdown>
21
+ </MaskShadow>
22
+ <Flexbox padding={4}>
23
+ <Button
24
+ block
25
+ color={'default'}
26
+ onClick={() => {
27
+ toggleMessageCollapsed(id, false);
28
+ }}
29
+ size={'small'}
30
+ variant={'filled'}
31
+ >
32
+ {t('chatList.expandMessage')}
33
+ </Button>
34
+ </Flexbox>
35
+ </Flexbox>
36
+ );
37
+ });
@@ -4,6 +4,10 @@ import isEqual from 'fast-deep-equal';
4
4
  import { memo, useMemo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
+ import { CollapsedMessage } from '@/features/Conversation/Messages/Group/CollapsedMessage';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { messageStateSelectors } from '@/store/chat/slices/message/selectors';
10
+
7
11
  import { GroupMessageContext } from './GroupContext';
8
12
  import GroupItem from './GroupItem';
9
13
 
@@ -19,18 +23,28 @@ const useStyles = createStyles(({ css }) => {
19
23
 
20
24
  interface GroupChildrenProps {
21
25
  blocks: AssistantContentBlock[];
26
+ content?: string;
22
27
  contentId?: string;
23
28
  disableEditing?: boolean;
24
29
  id: string;
25
30
  messageIndex: number;
26
31
  }
27
32
 
28
- const GroupChildren = memo<GroupChildrenProps>(
29
- ({ blocks, contentId, disableEditing, messageIndex, id }) => {
33
+ const Group = memo<GroupChildrenProps>(
34
+ ({ blocks, contentId, disableEditing, messageIndex, id, content }) => {
30
35
  const { styles } = useStyles();
31
-
36
+ const [isCollapsed] = useChatStore((s) => [messageStateSelectors.isMessageCollapsed(id)(s)]);
32
37
  const contextValue = useMemo(() => ({ assistantGroupId: id }), [id]);
33
38
 
39
+ if (isCollapsed) {
40
+ return (
41
+ content && (
42
+ <Flexbox>
43
+ <CollapsedMessage content={content} id={id} />
44
+ </Flexbox>
45
+ )
46
+ );
47
+ }
34
48
  return (
35
49
  <GroupMessageContext value={contextValue}>
36
50
  <Flexbox className={styles.container} gap={8}>
@@ -53,4 +67,4 @@ const GroupChildren = memo<GroupChildrenProps>(
53
67
  isEqual,
54
68
  );
55
69
 
56
- export default GroupChildren;
70
+ export default Group;
@@ -10,22 +10,19 @@ import Avatar from '@/features/ChatItem/components/Avatar';
10
10
  import BorderSpacing from '@/features/ChatItem/components/BorderSpacing';
11
11
  import Title from '@/features/ChatItem/components/Title';
12
12
  import { useStyles } from '@/features/ChatItem/style';
13
- import GroupChildren from '@/features/Conversation/Messages/Group/GroupChildren';
14
13
  import Usage from '@/features/Conversation/components/Extras/Usage';
15
14
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
16
15
  import { useAgentStore } from '@/store/agent';
17
16
  import { agentChatConfigSelectors } from '@/store/agent/selectors';
18
17
  import { useChatStore } from '@/store/chat';
19
- import {
20
- displayMessageSelectors,
21
- messageStateSelectors,
22
- } from '@/store/chat/slices/message/selectors';
18
+ import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
23
19
  import { useGlobalStore } from '@/store/global';
24
20
  import { useSessionStore } from '@/store/session';
25
21
  import { sessionSelectors } from '@/store/session/selectors';
26
22
 
27
23
  import { GroupActionsBar } from './Actions';
28
24
  import EditState from './EditState';
25
+ import Group from './Group';
29
26
 
30
27
  const MOBILE_AVATAR_SIZE = 32;
31
28
 
@@ -111,8 +108,9 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing }) =>
111
108
  width={'100%'}
112
109
  >
113
110
  {children && children.length > 0 && (
114
- <GroupChildren
111
+ <Group
115
112
  blocks={children}
113
+ content={lastAssistantMsg?.content}
116
114
  contentId={contentId}
117
115
  disableEditing={disableEditing}
118
116
  id={id}
@@ -6,6 +6,8 @@ import {
6
6
  DownloadIcon,
7
7
  Edit,
8
8
  LanguagesIcon,
9
+ ListChevronsDownUp,
10
+ ListChevronsUpDown,
9
11
  ListRestart,
10
12
  Play,
11
13
  RotateCcw,
@@ -27,12 +29,14 @@ const translateStyle = css`
27
29
 
28
30
  interface ChatListActionsBar {
29
31
  branching: ActionIconGroupItemType;
32
+ collapse: ActionIconGroupItemType;
30
33
  continueGeneration: ActionIconGroupItemType;
31
34
  copy: ActionIconGroupItemType;
32
35
  del: ActionIconGroupItemType;
33
36
  delAndRegenerate: ActionIconGroupItemType;
34
37
  divider: { type: 'divider' };
35
38
  edit: ActionIconGroupItemType;
39
+ expand: ActionIconGroupItemType;
36
40
  export: ActionIconGroupItemType;
37
41
  regenerate: ActionIconGroupItemType;
38
42
  share: ActionIconGroupItemType;
@@ -58,6 +62,11 @@ export const useChatListActionsBar = ({
58
62
  key: 'branching',
59
63
  label: t('branching'),
60
64
  },
65
+ collapse: {
66
+ icon: ListChevronsDownUp,
67
+ key: 'collapse',
68
+ label: t('messageAction.collapse', { ns: 'chat' }),
69
+ },
61
70
  continueGeneration: {
62
71
  disabled: isContinuing,
63
72
  icon: ArrowDownFromLine,
@@ -93,6 +102,11 @@ export const useChatListActionsBar = ({
93
102
  key: 'edit',
94
103
  label: t('edit'),
95
104
  },
105
+ expand: {
106
+ icon: ListChevronsUpDown,
107
+ key: 'expand',
108
+ label: t('messageAction.expand', { ns: 'chat' }),
109
+ },
96
110
  export: {
97
111
  icon: DownloadIcon,
98
112
  key: 'export',
@@ -106,7 +106,7 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
106
106
  // 初始化 OIDC 客户端(仅在客户端)
107
107
  useEffect(() => {
108
108
  if (typeof window !== 'undefined') {
109
- const baseUrl = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'http://127.0.0.1:8787';
109
+ const baseUrl = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
110
110
  const desktopRedirectUri = new URL(MARKET_OIDC_ENDPOINTS.desktopCallback, baseUrl).toString();
111
111
 
112
112
  // 桌面端使用 Market 手动维护的 Web 回调,Web 端使用当前域名
@@ -21,16 +21,16 @@ describe('MCPClient', () => {
21
21
  await mcpClient.initialize();
22
22
  // Add a small delay to allow the server process to fully start (optional, but can help)
23
23
  await new Promise((resolve) => setTimeout(resolve, 100));
24
- });
24
+ }, 30000);
25
25
 
26
26
  afterEach(async () => {
27
27
  // Assume SDK client/transport handles process termination gracefully
28
28
  // If processes leak, more explicit cleanup might be needed here
29
- });
29
+ }, 30000);
30
30
 
31
31
  it('should create and initialize an instance with stdio transport', () => {
32
32
  expect(mcpClient).toBeInstanceOf(MCPClient);
33
- });
33
+ }, 30000);
34
34
 
35
35
  it('should list tools via stdio', async () => {
36
36
  const result = await mcpClient.listTools();
@@ -40,7 +40,7 @@ describe('MCPClient', () => {
40
40
 
41
41
  // Expect the tools defined in mock-sdk-server.ts
42
42
  expect(result).toMatchSnapshot();
43
- });
43
+ }, 30000);
44
44
 
45
45
  it('should call the "echo" tool via stdio', async () => {
46
46
  const toolName = 'echo';
@@ -52,7 +52,7 @@ describe('MCPClient', () => {
52
52
 
53
53
  const result = await mcpClient.callTool(toolName, toolArgs);
54
54
  expect(result).toEqual(expectedResult);
55
- });
55
+ }, 30000);
56
56
 
57
57
  it('should call the "add" tool via stdio', async () => {
58
58
  const toolName = 'add';
@@ -62,7 +62,7 @@ describe('MCPClient', () => {
62
62
  expect(result).toEqual({
63
63
  content: [{ type: 'text', text: 'The sum is: 12' }],
64
64
  });
65
- });
65
+ }, 30000);
66
66
  });
67
67
 
68
68
  // Error Handling tests remain the same...
@@ -18,6 +18,7 @@ export default {
18
18
  availableAgents: '可用助手',
19
19
  backToBottom: '跳转至当前',
20
20
  chatList: {
21
+ expandMessage: '展开消息',
21
22
  longMessageDetail: '查看详情',
22
23
  },
23
24
  clearCurrentMessages: '清空当前会话消息',
@@ -188,9 +189,11 @@ export default {
188
189
  },
189
190
 
190
191
  messageAction: {
192
+ collapse: '收起消息',
191
193
  continueGeneration: '继续生成',
192
194
  delAndRegenerate: '删除并重新生成',
193
195
  deleteDisabledByThreads: '存在子话题,不能删除',
196
+ expand: '展开消息',
194
197
  regenerate: '重新生成',
195
198
  },
196
199
 
@@ -43,6 +43,10 @@ export interface MessagePublicApiAction {
43
43
  updateMessageInput: (message: string) => void;
44
44
  modifyMessageContent: (id: string, content: string) => Promise<void>;
45
45
  toggleMessageEditing: (id: string, editing: boolean) => void;
46
+ /**
47
+ * Toggle message collapsed state
48
+ */
49
+ toggleMessageCollapsed: (id: string, collapsed?: boolean) => Promise<void>;
46
50
 
47
51
  // ===== Others ===== //
48
52
  copyMessage: (id: string, content: string) => Promise<void>;
@@ -241,4 +245,17 @@ export const messagePublicApi: StateCreator<
241
245
 
242
246
  await get().optimisticUpdateMessageContent(id, content);
243
247
  },
248
+
249
+ toggleMessageCollapsed: async (id, collapsed) => {
250
+ const message = displayMessageSelectors.getDisplayMessageById(id)(get());
251
+ if (!message) return;
252
+
253
+ // 如果没有传入 collapsed,则取反当前状态
254
+ const nextCollapsed = collapsed ?? !message.metadata?.collapsed;
255
+
256
+ // 直接调用现有的 optimisticUpdateMessageMetadata
257
+ await get().optimisticUpdateMessageMetadata(id, {
258
+ collapsed: nextCollapsed,
259
+ });
260
+ },
244
261
  });
@@ -85,7 +85,7 @@ const activeDisplayMessages = (s: ChatStoreState): UIChatMessage[] => {
85
85
  /**
86
86
  * Get display message by ID (searches in messagesMap including assistantGroup children)
87
87
  */
88
- const getDisplayMessageById = (id: string) => (s: ChatStoreState) =>
88
+ export const getDisplayMessageById = (id: string) => (s: ChatStoreState) =>
89
89
  chatHelpers.getMessageById(activeDisplayMessages(s), id);
90
90
 
91
91
  const lastDisplayMessageId = (s: ChatStoreState) => {
@@ -1,6 +1,7 @@
1
1
  import type { ChatStoreState } from '../../../initialState';
2
2
  import { mainDisplayChatIDs } from './chat';
3
3
  import { getDbMessageByToolCallId } from './dbMessage';
4
+ import { getDisplayMessageById } from './displayMessage';
4
5
 
5
6
  const isMessageEditing = (id: string) => (s: ChatStoreState) => s.messageEditingIds.includes(id);
6
7
  const isMessageLoading = (id: string) => (s: ChatStoreState) => s.messageLoadingIds.includes(id);
@@ -13,6 +14,11 @@ const isMessageInRAGFlow = (id: string) => (s: ChatStoreState) =>
13
14
  const isMessageInChatReasoning = (id: string) => (s: ChatStoreState) =>
14
15
  s.reasoningLoadingIds.includes(id);
15
16
 
17
+ const isMessageCollapsed = (id: string) => (s: ChatStoreState) => {
18
+ const message = getDisplayMessageById(id)(s);
19
+ return message?.metadata?.collapsed ?? false;
20
+ };
21
+
16
22
  const isPluginApiInvoking = (id: string) => (s: ChatStoreState) =>
17
23
  s.pluginApiLoadingIds.includes(id);
18
24
 
@@ -71,6 +77,7 @@ export const messageStateSelectors = {
71
77
  isHasMessageLoading,
72
78
  isInRAGFlow,
73
79
  isInToolsCalling,
80
+ isMessageCollapsed,
74
81
  isMessageContinuing,
75
82
  isMessageEditing,
76
83
  isMessageGenerating,
@@ -1,5 +1,3 @@
1
- import { chainLangDetect } from '@lobechat/prompts';
2
- import { chainTranslate } from '@lobechat/prompts';
3
1
  import { act, renderHook } from '@testing-library/react';
4
2
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
3
 
@@ -9,7 +7,7 @@ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
9
7
 
10
8
  import { useChatStore } from '../../store';
11
9
 
12
- // Mock messageService chatService
10
+ // Mock messageService and chatService
13
11
  vi.mock('@/services/message', () => ({
14
12
  messageService: {
15
13
  updateMessageTTS: vi.fn(),
@@ -24,27 +22,20 @@ vi.mock('@/services/chat', () => ({
24
22
  },
25
23
  }));
26
24
 
27
- vi.mock('@/chains/langDetect', () => ({
28
- chainLangDetect: vi.fn(),
29
- }));
30
-
31
- vi.mock('@/chains/translate', () => ({
32
- chainTranslate: vi.fn(),
25
+ vi.mock('@/store/user', () => ({
26
+ useUserStore: {
27
+ getState: vi.fn(() => ({})),
28
+ },
33
29
  }));
34
30
 
35
- // Mock supportLocales
36
- vi.mock('@/locales/options', () => ({
37
- supportLocales: ['en-US', 'zh-CN'],
31
+ vi.mock('@/store/user/selectors', () => ({
32
+ systemAgentSelectors: {
33
+ translation: vi.fn(() => ({})),
34
+ },
38
35
  }));
39
36
 
40
37
  beforeEach(() => {
41
38
  vi.clearAllMocks();
42
- useChatStore.setState(
43
- {
44
- // ... 初始状态
45
- },
46
- false,
47
- );
48
39
  });
49
40
 
50
41
  afterEach(() => {
@@ -53,26 +44,26 @@ afterEach(() => {
53
44
 
54
45
  describe('ChatEnhanceAction', () => {
55
46
  describe('translateMessage', () => {
56
- it('should translate a message to the target language and refresh messages', async () => {
57
- const { result } = renderHook(() => useChatStore());
47
+ it('should translate a message to the target language', async () => {
58
48
  const messageId = 'message-id';
59
49
  const targetLang = 'zh-CN';
60
50
  const messageContent = 'Hello World';
61
51
  const detectedLang = 'en-US';
52
+ const translatedText = '你好世界';
62
53
 
54
+ // Setup initial state
63
55
  act(() => {
64
56
  useChatStore.setState({
65
57
  activeId: 'session',
66
- messagesMap: {
58
+ dbMessagesMap: {
67
59
  [messageMapKey('session')]: [
68
60
  {
69
61
  id: messageId,
70
62
  content: messageContent,
71
63
  createdAt: Date.now(),
72
64
  updatedAt: Date.now(),
73
- role: 'user',
74
- sessionId: 'test',
75
- topicId: 'test',
65
+ role: 'assistant',
66
+ sessionId: 'session',
76
67
  meta: {},
77
68
  },
78
69
  ],
@@ -80,21 +71,24 @@ describe('ChatEnhanceAction', () => {
80
71
  });
81
72
  });
82
73
 
83
- (chatService.fetchPresetTaskResult as Mock).mockImplementation(({ params }) => {
84
- if (params === chainLangDetect(messageContent)) {
85
- return Promise.resolve(detectedLang);
86
- }
87
- if (params === chainTranslate(messageContent, targetLang)) {
88
- return Promise.resolve('Hola Mundo');
89
- }
90
- return Promise.resolve(undefined);
74
+ // First call for language detection
75
+ (chatService.fetchPresetTaskResult as Mock).mockImplementationOnce(async ({ onFinish }) => {
76
+ if (onFinish) await onFinish(detectedLang);
91
77
  });
92
78
 
79
+ // Second call for translation
80
+ (chatService.fetchPresetTaskResult as Mock).mockImplementationOnce(async ({ onFinish }) => {
81
+ if (onFinish) await onFinish(translatedText);
82
+ });
83
+
84
+ const { result } = renderHook(() => useChatStore());
85
+
93
86
  await act(async () => {
94
87
  await result.current.translateMessage(messageId, targetLang);
95
88
  });
96
89
 
97
90
  expect(messageService.updateMessageTranslate).toHaveBeenCalled();
91
+ expect(chatService.fetchPresetTaskResult).toHaveBeenCalledTimes(2);
98
92
  });
99
93
  });
100
94
 
@@ -1,16 +1,16 @@
1
1
  import { chainLangDetect, chainTranslate } from '@lobechat/prompts';
2
2
  import { ChatTranslate, TraceNameMap, TracePayload } from '@lobechat/types';
3
+ import { merge } from '@lobechat/utils';
3
4
  import { produce } from 'immer';
4
5
  import { StateCreator } from 'zustand/vanilla';
5
6
 
6
7
  import { supportLocales } from '@/locales/resources';
7
8
  import { chatService } from '@/services/chat';
8
9
  import { messageService } from '@/services/message';
9
- import { chatSelectors } from '@/store/chat/selectors';
10
+ import { dbMessageSelectors } from '@/store/chat/selectors';
10
11
  import { ChatStore } from '@/store/chat/store';
11
12
  import { useUserStore } from '@/store/user';
12
13
  import { systemAgentSelectors } from '@/store/user/selectors';
13
- import { merge } from '@/utils/merge';
14
14
  import { setNamespace } from '@/utils/storeDebug';
15
15
 
16
16
  const n = setNamespace('enhance');
@@ -43,7 +43,7 @@ export const chatTranslate: StateCreator<
43
43
  translateMessage: async (id, targetLang) => {
44
44
  const { internal_toggleChatLoading, updateMessageTranslate, internal_dispatchMessage } = get();
45
45
 
46
- const message = chatSelectors.getMessageById(id)(get());
46
+ const message = dbMessageSelectors.getDbMessageById(id)(get());
47
47
  if (!message) return;
48
48
 
49
49
  // Get current agent for translation