@lobehub/lobehub 2.0.0-next.7 → 2.0.0-next.9

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 (127) hide show
  1. package/.github/workflows/desktop-pr-build.yml +8 -8
  2. package/.github/workflows/docker.yml +17 -16
  3. package/.github/workflows/e2e.yml +3 -3
  4. package/.github/workflows/release-desktop-beta.yml +8 -8
  5. package/.github/workflows/release.yml +1 -1
  6. package/.github/workflows/test.yml +4 -4
  7. package/CHANGELOG.md +50 -0
  8. package/changelog/v1.json +18 -0
  9. package/locales/ar/models.json +6 -6
  10. package/locales/bg-BG/models.json +6 -6
  11. package/locales/de-DE/models.json +6 -6
  12. package/locales/en-US/models.json +6 -6
  13. package/locales/es-ES/models.json +6 -6
  14. package/locales/fa-IR/models.json +6 -6
  15. package/locales/fr-FR/models.json +6 -6
  16. package/locales/it-IT/models.json +6 -6
  17. package/locales/ja-JP/models.json +6 -6
  18. package/locales/ko-KR/models.json +6 -6
  19. package/locales/nl-NL/models.json +6 -6
  20. package/locales/pl-PL/models.json +6 -6
  21. package/locales/pt-BR/models.json +6 -6
  22. package/locales/ru-RU/models.json +6 -6
  23. package/locales/tr-TR/models.json +6 -6
  24. package/locales/vi-VN/models.json +6 -6
  25. package/locales/zh-CN/models.json +6 -6
  26. package/locales/zh-TW/models.json +6 -6
  27. package/package.json +1 -1
  28. package/packages/const/src/index.ts +0 -1
  29. package/packages/const/src/url.ts +1 -4
  30. package/packages/context-engine/src/index.ts +1 -6
  31. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +12 -2
  32. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +73 -9
  33. package/packages/context-engine/src/providers/index.ts +0 -2
  34. package/packages/database/package.json +1 -1
  35. package/packages/database/src/models/__tests__/message.grouping.test.ts +812 -0
  36. package/packages/database/src/models/__tests__/message.test.ts +322 -170
  37. package/packages/database/src/models/message.ts +62 -24
  38. package/packages/database/src/utils/__tests__/groupMessages.test.ts +145 -2
  39. package/packages/database/src/utils/groupMessages.ts +7 -5
  40. package/packages/types/src/message/common/base.ts +13 -0
  41. package/packages/types/src/message/common/image.ts +8 -0
  42. package/packages/types/src/message/common/metadata.ts +39 -0
  43. package/packages/types/src/message/common/tools.ts +10 -0
  44. package/packages/types/src/message/db/params.ts +47 -1
  45. package/packages/types/src/message/ui/chat.ts +4 -1
  46. package/packages/types/src/search.ts +16 -0
  47. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  48. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/useSend.ts +6 -4
  49. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +15 -10
  50. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +4 -2
  51. package/src/components/Thinking/index.tsx +4 -3
  52. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  53. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  54. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +1 -3
  56. package/src/features/Conversation/Error/ErrorJsonViewer.tsx +4 -3
  57. package/src/features/Conversation/Error/OllamaBizError/index.tsx +7 -2
  58. package/src/features/Conversation/Error/index.tsx +15 -5
  59. package/src/features/Conversation/MarkdownElements/LobeArtifact/Render/index.tsx +2 -2
  60. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -2
  61. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +5 -3
  62. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/BuiltinPluginTitle.tsx +2 -2
  63. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +4 -2
  64. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +2 -2
  65. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -2
  66. package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +2 -2
  67. package/src/features/Conversation/Messages/Assistant/index.tsx +4 -4
  68. package/src/features/Conversation/Messages/Default.tsx +2 -2
  69. package/src/features/Conversation/Messages/User/Extra.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/index.tsx +4 -4
  71. package/src/features/Conversation/Messages/index.tsx +3 -3
  72. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  73. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +9 -6
  74. package/src/features/PluginTag/index.tsx +1 -3
  75. package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +37 -28
  76. package/src/features/Portal/Artifacts/Body/index.tsx +2 -2
  77. package/src/server/modules/ModelRuntime/trace.ts +11 -4
  78. package/src/server/routers/lambda/message.ts +14 -3
  79. package/src/services/chat/chat.test.ts +1 -40
  80. package/src/services/chat/contextEngineering.test.ts +0 -30
  81. package/src/services/chat/contextEngineering.ts +1 -12
  82. package/src/services/chat/index.ts +2 -7
  83. package/src/services/chat/types.ts +1 -1
  84. package/src/services/message/_deprecated.ts +1 -1
  85. package/src/services/message/client.ts +8 -2
  86. package/src/services/message/server.ts +7 -2
  87. package/src/services/message/type.ts +6 -1
  88. package/src/store/chat/helpers.test.ts +99 -0
  89. package/src/store/chat/helpers.ts +21 -2
  90. package/src/store/chat/selectors.ts +1 -1
  91. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +3 -3
  92. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -4
  93. package/src/store/chat/slices/message/action.test.ts +5 -1
  94. package/src/store/chat/slices/message/action.ts +102 -14
  95. package/src/store/chat/slices/message/reducer.test.ts +363 -5
  96. package/src/store/chat/slices/message/reducer.ts +87 -3
  97. package/src/store/chat/slices/message/{selectors.test.ts → selectors/chat.test.ts} +266 -30
  98. package/src/store/chat/slices/message/{selectors.ts → selectors/chat.ts} +29 -79
  99. package/src/store/chat/slices/message/selectors/index.ts +2 -0
  100. package/src/store/chat/slices/message/selectors/messageState.test.ts +36 -0
  101. package/src/store/chat/slices/message/selectors/messageState.ts +80 -0
  102. package/src/store/chat/slices/plugin/action.test.ts +34 -132
  103. package/src/store/chat/slices/plugin/action.ts +1 -44
  104. package/src/store/tool/selectors/tool.test.ts +1 -1
  105. package/src/store/tool/selectors/tool.ts +6 -8
  106. package/src/store/tool/slices/builtin/action.test.ts +83 -35
  107. package/src/store/tool/slices/builtin/action.ts +0 -9
  108. package/src/store/tool/slices/builtin/selectors.test.ts +4 -30
  109. package/src/store/tool/slices/builtin/selectors.ts +15 -21
  110. package/src/tools/index.ts +0 -6
  111. package/src/tools/renders.ts +0 -3
  112. package/src/tools/web-browsing/Portal/Search/Footer.tsx +2 -2
  113. package/packages/const/src/guide.ts +0 -89
  114. package/packages/context-engine/src/providers/InboxGuide.ts +0 -102
  115. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +0 -121
  116. package/src/services/chat/__snapshots__/chat.test.ts.snap +0 -110
  117. package/src/store/chat/slices/builtinTool/actions/__tests__/dalle.test.ts +0 -121
  118. package/src/store/chat/slices/builtinTool/actions/dalle.ts +0 -124
  119. package/src/tools/dalle/Render/GalleyGrid.tsx +0 -60
  120. package/src/tools/dalle/Render/Item/EditMode.tsx +0 -66
  121. package/src/tools/dalle/Render/Item/Error.tsx +0 -49
  122. package/src/tools/dalle/Render/Item/Image.tsx +0 -44
  123. package/src/tools/dalle/Render/Item/ImageFileItem.tsx +0 -57
  124. package/src/tools/dalle/Render/Item/index.tsx +0 -88
  125. package/src/tools/dalle/Render/ToolBar.tsx +0 -56
  126. package/src/tools/dalle/Render/index.tsx +0 -52
  127. package/src/tools/dalle/index.ts +0 -92
@@ -29,7 +29,7 @@ import { useSessionStore } from '@/store/session';
29
29
  import { WebBrowsingManifest } from '@/tools/web-browsing';
30
30
  import { Action, setNamespace } from '@/utils/storeDebug';
31
31
 
32
- import { chatSelectors, topicSelectors } from '../../../selectors';
32
+ import { chatSelectors, messageStateSelectors, topicSelectors } from '../../../selectors';
33
33
 
34
34
  const n = setNamespace('ai');
35
35
 
@@ -598,7 +598,6 @@ export const generateAIChat: StateCreator<
598
598
  topicId: get().activeTopicId,
599
599
  traceName: TraceNameMap.Conversation,
600
600
  },
601
- isWelcomeQuestion: params?.isWelcomeQuestion,
602
601
  onErrorHandle: async (error) => {
603
602
  await messageService.updateMessageError(messageId, error);
604
603
  await refreshMessages();
@@ -707,7 +706,8 @@ export const generateAIChat: StateCreator<
707
706
  if (!duration) {
708
707
  duration = Date.now() - thinkingStartAt;
709
708
 
710
- const isInChatReasoning = chatSelectors.isMessageInChatReasoning(messageId)(get());
709
+ const isInChatReasoning =
710
+ messageStateSelectors.isMessageInChatReasoning(messageId)(get());
711
711
  if (isInChatReasoning) {
712
712
  internal_toggleChatReasoning(
713
713
  false,
@@ -2,14 +2,12 @@ import { StateCreator } from 'zustand/vanilla';
2
2
 
3
3
  import { ChatStore } from '@/store/chat/store';
4
4
 
5
- import { ChatDallEAction, dalleSlice } from './dalle';
6
5
  import { ChatCodeInterpreterAction, codeInterpreterSlice } from './interpreter';
7
6
  import { LocalFileAction, localSystemSlice } from './localSystem';
8
7
  import { SearchAction, searchSlice } from './search';
9
8
 
10
9
  export interface ChatBuiltinToolAction
11
- extends ChatDallEAction,
12
- SearchAction,
10
+ extends SearchAction,
13
11
  LocalFileAction,
14
12
  ChatCodeInterpreterAction {}
15
13
 
@@ -19,7 +17,6 @@ export const chatToolSlice: StateCreator<
19
17
  [],
20
18
  ChatBuiltinToolAction
21
19
  > = (...params) => ({
22
- ...dalleSlice(...params),
23
20
  ...searchSlice(...params),
24
21
  ...localSystemSlice(...params),
25
22
  ...codeInterpreterSlice(...params),
@@ -471,7 +471,11 @@ describe('chatMessage actions', () => {
471
471
  await result.current.internal_updateMessageContent(messageId, newContent);
472
472
  });
473
473
 
474
- expect(spy).toHaveBeenCalledWith(messageId, { content: newContent });
474
+ expect(spy).toHaveBeenCalledWith(
475
+ messageId,
476
+ { content: newContent },
477
+ { sessionId: 'session-id', topicId: 'topic-id' },
478
+ );
475
479
  });
476
480
 
477
481
  it('should dispatch message update action', async () => {
@@ -6,6 +6,7 @@ import {
6
6
  ChatMessageError,
7
7
  ChatMessagePluginError,
8
8
  CreateMessageParams,
9
+ CreateNewMessageParams,
9
10
  GroundingSearch,
10
11
  MessageMetadata,
11
12
  MessageToolCall,
@@ -117,6 +118,14 @@ export interface ChatMessageAction {
117
118
  * otherwise the message will be too slow to show
118
119
  */
119
120
  internal_createTmpMessage: (params: CreateMessageParams) => string;
121
+ /**
122
+ * create a new message using createNewMessage API and return full message list
123
+ * used for group message scenarios to reduce network requests
124
+ */
125
+ internal_createNewMessage: (
126
+ params: CreateNewMessageParams,
127
+ context?: { tempMessageId?: string; groupMessageId?: string },
128
+ ) => Promise<{ id: string; messages: UIChatMessage[] } | undefined>;
120
129
  /**
121
130
  * delete the message content with optimistic update
122
131
  */
@@ -374,8 +383,16 @@ export const chatMessage: StateCreator<
374
383
 
375
384
  internal_updateMessageError: async (id, error) => {
376
385
  get().internal_dispatchMessage({ id, type: 'updateMessage', value: { error } });
377
- await messageService.updateMessage(id, { error });
378
- await get().refreshMessages();
386
+ const result = await messageService.updateMessage(
387
+ id,
388
+ { error },
389
+ { topicId: get().activeTopicId, sessionId: get().activeId },
390
+ );
391
+ if (result?.success && result.messages) {
392
+ get().replaceMessages(result.messages);
393
+ } else {
394
+ await get().refreshMessages();
395
+ }
379
396
  },
380
397
 
381
398
  internal_updateMessagePluginError: async (id, error) => {
@@ -384,7 +401,12 @@ export const chatMessage: StateCreator<
384
401
  },
385
402
 
386
403
  internal_updateMessageContent: async (id, content, extra) => {
387
- const { internal_dispatchMessage, refreshMessages, internal_transformToolCalls } = get();
404
+ const {
405
+ internal_dispatchMessage,
406
+ refreshMessages,
407
+ internal_transformToolCalls,
408
+ replaceMessages,
409
+ } = get();
388
410
 
389
411
  // Due to the async update method and refresh need about 100ms
390
412
  // we need to update the message content at the frontend to avoid the update flick
@@ -403,17 +425,26 @@ export const chatMessage: StateCreator<
403
425
  });
404
426
  }
405
427
 
406
- await messageService.updateMessage(id, {
407
- content,
408
- tools: extra?.toolCalls ? internal_transformToolCalls(extra?.toolCalls) : undefined,
409
- reasoning: extra?.reasoning,
410
- search: extra?.search,
411
- metadata: extra?.metadata,
412
- model: extra?.model,
413
- provider: extra?.provider,
414
- imageList: extra?.imageList,
415
- });
416
- await refreshMessages();
428
+ const result = await messageService.updateMessage(
429
+ id,
430
+ {
431
+ content,
432
+ tools: extra?.toolCalls ? internal_transformToolCalls(extra?.toolCalls) : undefined,
433
+ reasoning: extra?.reasoning,
434
+ search: extra?.search,
435
+ metadata: extra?.metadata,
436
+ model: extra?.model,
437
+ provider: extra?.provider,
438
+ imageList: extra?.imageList,
439
+ },
440
+ { topicId: get().activeTopicId, sessionId: get().activeId },
441
+ );
442
+
443
+ if (result && result.success && result.messages) {
444
+ replaceMessages(result.messages);
445
+ } else {
446
+ await refreshMessages();
447
+ }
417
448
  },
418
449
 
419
450
  internal_createMessage: async (message, context) => {
@@ -473,6 +504,63 @@ export const chatMessage: StateCreator<
473
504
 
474
505
  return tempId;
475
506
  },
507
+
508
+ internal_createNewMessage: async (message, context) => {
509
+ const {
510
+ internal_createTmpMessage,
511
+ internal_toggleMessageLoading,
512
+ internal_dispatchMessage,
513
+ replaceMessages,
514
+ } = get();
515
+
516
+ let tempId = context?.tempMessageId;
517
+ if (!tempId) {
518
+ tempId = 'tmp_' + nanoid();
519
+
520
+ // Check if should add as group block (explicitly controlled by caller)
521
+ if (context?.groupMessageId) {
522
+ internal_dispatchMessage({
523
+ type: 'addGroupBlock',
524
+ groupMessageId: context.groupMessageId,
525
+ blockId: tempId,
526
+ value: {
527
+ id: tempId,
528
+ content: message.content,
529
+ },
530
+ });
531
+ internal_toggleMessageLoading(true, tempId);
532
+ } else {
533
+ // Regular message creation at top level
534
+ tempId = internal_createTmpMessage(message as any);
535
+ internal_toggleMessageLoading(true, tempId);
536
+ }
537
+ }
538
+
539
+ try {
540
+ // 使用 createNewMessage API
541
+ const result = await messageService.createNewMessage(message);
542
+
543
+ // 直接用返回的 messages 更新 store(已包含 group 结构)
544
+ replaceMessages(result.messages);
545
+
546
+ internal_toggleMessageLoading(false, tempId);
547
+ return result;
548
+ } catch (e) {
549
+ internal_toggleMessageLoading(false, tempId);
550
+ internal_dispatchMessage({
551
+ id: tempId,
552
+ type: 'updateMessage',
553
+ value: {
554
+ error: {
555
+ type: ChatErrorType.CreateMessageError,
556
+ message: (e as Error).message,
557
+ body: e,
558
+ },
559
+ },
560
+ });
561
+ }
562
+ },
563
+
476
564
  internal_deleteMessage: async (id: string) => {
477
565
  get().internal_dispatchMessage({ type: 'deleteMessage', id });
478
566
  await messageService.removeMessage(id);
@@ -57,16 +57,131 @@ describe('messagesReducer', () => {
57
57
  expect(newState).toEqual(initialState);
58
58
  });
59
59
 
60
- it('should not modify the state if the specified message does not exist', () => {
60
+ it('should update a block in group message children when id matches a block', () => {
61
+ const stateWithGroup: UIChatMessage[] = [
62
+ ...initialState,
63
+ {
64
+ id: 'group1',
65
+ role: 'group',
66
+ content: '',
67
+ createdAt: 1629264000000,
68
+ updatedAt: 1629264000000,
69
+ meta: {},
70
+ children: [
71
+ {
72
+ id: 'block1',
73
+ content: 'Original block content',
74
+ tools: [
75
+ {
76
+ id: 'tool1',
77
+ identifier: 'search',
78
+ apiName: 'search',
79
+ type: 'builtin',
80
+ arguments: '{"query": "test"}',
81
+ },
82
+ ],
83
+ },
84
+ ],
85
+ } as UIChatMessage,
86
+ ];
87
+
61
88
  const payload: MessageDispatch = {
62
89
  type: 'updateMessage',
63
- id: 'nonexistentMessage',
64
- value: { content: 'Updated Message' },
90
+ id: 'block1',
91
+ value: { content: 'Updated block content' },
65
92
  };
66
93
 
67
- const newState = messagesReducer(initialState, payload);
94
+ const newState = messagesReducer(stateWithGroup, payload);
95
+ const groupMessage = newState.find((m) => m.id === 'group1');
96
+ const block = groupMessage?.children?.find((b) => b.id === 'block1');
68
97
 
69
- expect(newState).toEqual(initialState);
98
+ expect(block?.content).toBe('Updated block content');
99
+ expect(groupMessage?.updatedAt).toBeGreaterThan(1629264000000);
100
+ });
101
+
102
+ it('should update block tools in group message children', () => {
103
+ const stateWithGroup: UIChatMessage[] = [
104
+ ...initialState,
105
+ {
106
+ id: 'group1',
107
+ role: 'group',
108
+ content: '',
109
+ createdAt: 1629264000000,
110
+ updatedAt: 1629264000000,
111
+ meta: {},
112
+ children: [
113
+ {
114
+ id: 'block1',
115
+ content: 'Block content',
116
+ tools: [
117
+ {
118
+ id: 'tool1',
119
+ identifier: 'search',
120
+ apiName: 'search',
121
+ type: 'builtin',
122
+ arguments: '{"query": "test"}',
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ } as UIChatMessage,
128
+ ];
129
+
130
+ const newTools = [
131
+ {
132
+ id: 'tool1',
133
+ identifier: 'search',
134
+ apiName: 'search',
135
+ type: 'builtin',
136
+ arguments: '{"query": "updated"}',
137
+ result: {
138
+ id: 'result1',
139
+ content: 'Search result',
140
+ },
141
+ },
142
+ ];
143
+
144
+ const payload: MessageDispatch = {
145
+ type: 'updateMessage',
146
+ id: 'block1',
147
+ value: { tools: newTools as any },
148
+ };
149
+
150
+ const newState = messagesReducer(stateWithGroup, payload);
151
+ const groupMessage = newState.find((m) => m.id === 'group1');
152
+ const block = groupMessage?.children?.find((b) => b.id === 'block1');
153
+
154
+ expect(block?.tools).toEqual(newTools);
155
+ expect(groupMessage?.updatedAt).toBeGreaterThan(1629264000000);
156
+ });
157
+
158
+ it('should not modify state when updating non-existent block in group message', () => {
159
+ const stateWithGroup: UIChatMessage[] = [
160
+ ...initialState,
161
+ {
162
+ id: 'group1',
163
+ role: 'group',
164
+ content: '',
165
+ createdAt: 1629264000000,
166
+ updatedAt: 1629264000000,
167
+ meta: {},
168
+ children: [
169
+ {
170
+ id: 'block1',
171
+ content: 'Block content',
172
+ },
173
+ ],
174
+ } as UIChatMessage,
175
+ ];
176
+
177
+ const payload: MessageDispatch = {
178
+ type: 'updateMessage',
179
+ id: 'nonexistentBlock',
180
+ value: { content: 'Updated content' },
181
+ };
182
+
183
+ const newState = messagesReducer(stateWithGroup, payload);
184
+ expect(newState).toEqual(stateWithGroup);
70
185
  });
71
186
  });
72
187
 
@@ -502,4 +617,247 @@ describe('messagesReducer', () => {
502
617
  expect(newState).toEqual(initialState);
503
618
  });
504
619
  });
620
+
621
+ describe('updateGroupBlockToolResult', () => {
622
+ it('should update a tool result in a group message block', () => {
623
+ const stateWithGroup: UIChatMessage[] = [
624
+ ...initialState,
625
+ {
626
+ id: 'group1',
627
+ role: 'group',
628
+ content: '',
629
+ createdAt: 1629264000000,
630
+ updatedAt: 1629264000000,
631
+ meta: {},
632
+ children: [
633
+ {
634
+ id: 'block1',
635
+ content: 'Assistant response',
636
+ tools: [
637
+ {
638
+ id: 'tool1',
639
+ identifier: 'search',
640
+ apiName: 'search',
641
+ type: 'builtin',
642
+ arguments: '{"query": "test"}',
643
+ result: {
644
+ id: 'result1',
645
+ content: 'Initial result',
646
+ },
647
+ },
648
+ ],
649
+ },
650
+ ],
651
+ } as UIChatMessage,
652
+ ];
653
+
654
+ const payload: MessageDispatch = {
655
+ type: 'updateGroupBlockToolResult',
656
+ groupMessageId: 'group1',
657
+ blockId: 'block1',
658
+ toolId: 'tool1',
659
+ toolResult: {
660
+ id: 'result1',
661
+ content: 'Updated result content',
662
+ state: { foo: 'bar' },
663
+ },
664
+ };
665
+
666
+ const newState = messagesReducer(stateWithGroup, payload);
667
+ const groupMessage = newState.find((m) => m.id === 'group1');
668
+ const block = groupMessage?.children?.find((b) => b.id === 'block1');
669
+ const tool = block?.tools?.find((t) => t.id === 'tool1');
670
+
671
+ expect(tool?.result).toEqual({
672
+ id: 'result1',
673
+ content: 'Updated result content',
674
+ state: { foo: 'bar' },
675
+ });
676
+ expect(groupMessage?.updatedAt).toBeGreaterThan(1629264000000);
677
+ });
678
+
679
+ it('should not modify state if group message is not found', () => {
680
+ const payload: MessageDispatch = {
681
+ type: 'updateGroupBlockToolResult',
682
+ groupMessageId: 'nonexistent',
683
+ blockId: 'block1',
684
+ toolId: 'tool1',
685
+ toolResult: {
686
+ id: 'result1',
687
+ content: 'Updated result',
688
+ },
689
+ };
690
+
691
+ const newState = messagesReducer(initialState, payload);
692
+ expect(newState).toEqual(initialState);
693
+ });
694
+
695
+ it('should not modify state if block is not found', () => {
696
+ const stateWithGroup: UIChatMessage[] = [
697
+ ...initialState,
698
+ {
699
+ id: 'group1',
700
+ role: 'group',
701
+ content: '',
702
+ createdAt: 1629264000000,
703
+ updatedAt: 1629264000000,
704
+ meta: {},
705
+ children: [
706
+ {
707
+ id: 'block1',
708
+ content: 'Assistant response',
709
+ tools: [],
710
+ },
711
+ ],
712
+ } as UIChatMessage,
713
+ ];
714
+
715
+ const payload: MessageDispatch = {
716
+ type: 'updateGroupBlockToolResult',
717
+ groupMessageId: 'group1',
718
+ blockId: 'nonexistentBlock',
719
+ toolId: 'tool1',
720
+ toolResult: {
721
+ id: 'result1',
722
+ content: 'Updated result',
723
+ },
724
+ };
725
+
726
+ const newState = messagesReducer(stateWithGroup, payload);
727
+ expect(newState).toEqual(stateWithGroup);
728
+ });
729
+
730
+ it('should not modify state if tool is not found', () => {
731
+ const stateWithGroup: UIChatMessage[] = [
732
+ ...initialState,
733
+ {
734
+ id: 'group1',
735
+ role: 'group',
736
+ content: '',
737
+ createdAt: 1629264000000,
738
+ updatedAt: 1629264000000,
739
+ meta: {},
740
+ children: [
741
+ {
742
+ id: 'block1',
743
+ content: 'Assistant response',
744
+ tools: [
745
+ {
746
+ id: 'tool1',
747
+ identifier: 'search',
748
+ apiName: 'search',
749
+ type: 'builtin',
750
+ arguments: '{"query": "test"}',
751
+ },
752
+ ],
753
+ },
754
+ ],
755
+ } as UIChatMessage,
756
+ ];
757
+
758
+ const payload: MessageDispatch = {
759
+ type: 'updateGroupBlockToolResult',
760
+ groupMessageId: 'group1',
761
+ blockId: 'block1',
762
+ toolId: 'nonexistentTool',
763
+ toolResult: {
764
+ id: 'result1',
765
+ content: 'Updated result',
766
+ },
767
+ };
768
+
769
+ const newState = messagesReducer(stateWithGroup, payload);
770
+ expect(newState).toEqual(stateWithGroup);
771
+ });
772
+ });
773
+
774
+ describe('addGroupBlock', () => {
775
+ it('should add a new block to group message children', () => {
776
+ const stateWithGroup: UIChatMessage[] = [
777
+ ...initialState,
778
+ {
779
+ id: 'group1',
780
+ role: 'group',
781
+ content: '',
782
+ createdAt: 1629264000000,
783
+ updatedAt: 1629264000000,
784
+ meta: {},
785
+ children: [
786
+ {
787
+ id: 'block1',
788
+ content: 'First block',
789
+ },
790
+ ],
791
+ } as UIChatMessage,
792
+ ];
793
+
794
+ const payload: MessageDispatch = {
795
+ type: 'addGroupBlock',
796
+ groupMessageId: 'group1',
797
+ blockId: 'block2',
798
+ value: {
799
+ id: 'block2',
800
+ content: 'Second block',
801
+ },
802
+ };
803
+
804
+ const newState = messagesReducer(stateWithGroup, payload);
805
+ const groupMessage = newState.find((m) => m.id === 'group1');
806
+
807
+ expect(groupMessage?.children).toHaveLength(2);
808
+ expect(groupMessage?.children?.[1]).toEqual({
809
+ id: 'block2',
810
+ content: 'Second block',
811
+ });
812
+ expect(groupMessage?.updatedAt).toBeGreaterThan(1629264000000);
813
+ });
814
+
815
+ it('should not modify state if group message is not found', () => {
816
+ const stateWithGroup: UIChatMessage[] = [
817
+ ...initialState,
818
+ {
819
+ id: 'group1',
820
+ role: 'group',
821
+ content: '',
822
+ createdAt: 1629264000000,
823
+ updatedAt: 1629264000000,
824
+ meta: {},
825
+ children: [
826
+ {
827
+ id: 'block1',
828
+ content: 'First block',
829
+ },
830
+ ],
831
+ } as UIChatMessage,
832
+ ];
833
+
834
+ const payload: MessageDispatch = {
835
+ type: 'addGroupBlock',
836
+ groupMessageId: 'nonexistentGroup',
837
+ blockId: 'block2',
838
+ value: {
839
+ id: 'block2',
840
+ content: 'Second block',
841
+ },
842
+ };
843
+
844
+ const newState = messagesReducer(stateWithGroup, payload);
845
+ expect(newState).toEqual(stateWithGroup);
846
+ });
847
+
848
+ it('should not modify state if message is not a group message', () => {
849
+ const payload: MessageDispatch = {
850
+ type: 'addGroupBlock',
851
+ groupMessageId: 'message1',
852
+ blockId: 'block2',
853
+ value: {
854
+ id: 'block2',
855
+ content: 'Second block',
856
+ },
857
+ };
858
+
859
+ const newState = messagesReducer(initialState, payload);
860
+ expect(newState).toEqual(initialState);
861
+ });
862
+ });
505
863
  });