@lobehub/chat 1.2.5 → 1.2.7

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 (33) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +1 -1
  3. package/src/app/(main)/chat/(workspace)/@portal/features/ArtifactUI/Footer.tsx +1 -1
  4. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +1 -1
  5. package/src/app/(main)/chat/(workspace)/_layout/Mobile/PortalModal.tsx +35 -0
  6. package/src/app/(main)/chat/(workspace)/_layout/Mobile/index.tsx +3 -1
  7. package/src/database/client/models/__tests__/message.test.ts +13 -0
  8. package/src/database/client/models/message.ts +4 -0
  9. package/src/database/server/models/__tests__/message.test.ts +103 -0
  10. package/src/database/server/models/message.ts +13 -6
  11. package/src/features/Conversation/Actions/Tool.tsx +12 -6
  12. package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +25 -19
  13. package/src/features/Conversation/Messages/Tool/Inspector/style.ts +9 -0
  14. package/src/layout/AuthProvider/Clerk/index.tsx +3 -3
  15. package/src/server/routers/lambda/message.ts +7 -1
  16. package/src/services/message/client.test.ts +16 -2
  17. package/src/services/message/client.ts +5 -1
  18. package/src/services/message/server.ts +7 -2
  19. package/src/services/message/type.ts +2 -1
  20. package/src/store/chat/slices/message/action.test.ts +144 -0
  21. package/src/store/chat/slices/message/action.ts +111 -64
  22. package/src/store/chat/slices/message/reducer.test.ts +200 -1
  23. package/src/store/chat/slices/message/reducer.ts +62 -2
  24. package/src/store/chat/slices/plugin/action.test.ts +42 -0
  25. package/src/store/chat/slices/plugin/action.ts +46 -0
  26. package/src/store/chat/slices/portal/action.test.ts +6 -6
  27. package/src/store/chat/slices/portal/action.ts +3 -3
  28. package/src/store/chat/slices/topic/action.test.ts +3 -2
  29. package/src/store/chat/slices/topic/action.ts +1 -1
  30. package/src/store/global/action.test.ts +13 -0
  31. package/src/store/global/action.ts +7 -0
  32. package/src/store/global/initialState.ts +1 -0
  33. package/src/store/global/selectors.ts +2 -0
@@ -2,7 +2,7 @@ import isEqual from 'fast-deep-equal';
2
2
  import { produce } from 'immer';
3
3
 
4
4
  import { CreateMessageParams } from '@/services/message';
5
- import { ChatMessage, ChatPluginPayload } from '@/types/message';
5
+ import { ChatMessage, ChatPluginPayload, ChatToolPayload } from '@/types/message';
6
6
  import { merge } from '@/utils/merge';
7
7
 
8
8
  interface UpdateMessages {
@@ -16,11 +16,17 @@ interface CreateMessage {
16
16
  type: 'createMessage';
17
17
  value: CreateMessageParams;
18
18
  }
19
+
19
20
  interface DeleteMessage {
20
21
  id: string;
21
22
  type: 'deleteMessage';
22
23
  }
23
24
 
25
+ interface DeleteMessages {
26
+ ids: string[];
27
+ type: 'deleteMessages';
28
+ }
29
+
24
30
  interface UpdatePluginState {
25
31
  id: string;
26
32
  key: string;
@@ -41,6 +47,17 @@ interface UpdateMessageTools {
41
47
  value: Partial<ChatPluginPayload>;
42
48
  }
43
49
 
50
+ interface AddMessageTool {
51
+ id: string;
52
+ type: 'addMessageTool';
53
+ value: ChatToolPayload;
54
+ }
55
+ interface DeleteMessageTool {
56
+ id: string;
57
+ tool_call_id: string;
58
+ type: 'deleteMessageTool';
59
+ }
60
+
44
61
  interface UpdateMessageExtra {
45
62
  id: string;
46
63
  key: string;
@@ -55,7 +72,10 @@ export type MessageDispatch =
55
72
  | UpdateMessageExtra
56
73
  | DeleteMessage
57
74
  | UpdateMessagePlugin
58
- | UpdateMessageTools;
75
+ | UpdateMessageTools
76
+ | AddMessageTool
77
+ | DeleteMessageTool
78
+ | DeleteMessages;
59
79
 
60
80
  export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
61
81
  switch (payload.type) {
@@ -116,6 +136,37 @@ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch):
116
136
  });
117
137
  }
118
138
 
139
+ case 'addMessageTool': {
140
+ return produce(state, (draftState) => {
141
+ const { id, value } = payload;
142
+ const message = draftState.find((i) => i.id === id);
143
+ if (!message || message.role !== 'assistant') return;
144
+
145
+ if (!message.tools) {
146
+ message.tools = [value];
147
+ } else {
148
+ const index = message.tools.findIndex((tool) => tool.id === value.id);
149
+
150
+ if (index > 0) return;
151
+ message.tools.push(value);
152
+ }
153
+
154
+ message.updatedAt = Date.now();
155
+ });
156
+ }
157
+
158
+ case 'deleteMessageTool': {
159
+ return produce(state, (draftState) => {
160
+ const { id, tool_call_id } = payload;
161
+ const message = draftState.find((i) => i.id === id);
162
+ if (!message || message.role !== 'assistant' || !message.tools) return;
163
+
164
+ message.tools = message.tools.filter((tool) => tool.id !== tool_call_id);
165
+
166
+ message.updatedAt = Date.now();
167
+ });
168
+ }
169
+
119
170
  case 'updateMessageTools': {
120
171
  return produce(state, (draftState) => {
121
172
  const { id, value, tool_call_id } = payload;
@@ -147,6 +198,15 @@ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch):
147
198
  if (index >= 0) draft.splice(index, 1);
148
199
  });
149
200
  }
201
+ case 'deleteMessages': {
202
+ return produce(state, (draft) => {
203
+ const { ids } = payload;
204
+
205
+ return draft.filter((item) => {
206
+ return !ids.includes(item.id);
207
+ });
208
+ });
209
+ }
150
210
  default: {
151
211
  throw new Error('暂未实现的 type,请检查 reducer');
152
212
  }
@@ -964,4 +964,46 @@ describe('ChatPluginAction', () => {
964
964
  expect(result.current.refreshMessages).toHaveBeenCalled();
965
965
  });
966
966
  });
967
+
968
+ describe('internal_addToolToAssistantMessage', () => {
969
+ it('should add too to assistant messages', async () => {
970
+ const { result } = renderHook(() => useChatStore());
971
+
972
+ const messageId = 'message-id';
973
+ const toolCallId = 'tool-call-id';
974
+ const identifier = 'plugin';
975
+
976
+ const refreshToUpdateMessageToolsSpy = vi.spyOn(
977
+ result.current,
978
+ 'internal_refreshToUpdateMessageTools',
979
+ );
980
+
981
+ const assistantMessage = {
982
+ id: messageId,
983
+ role: 'assistant',
984
+ content: 'Assistant content',
985
+ tools: [{ identifier: identifier, arguments: '{"oldKey":"oldValue"}', id: toolCallId }],
986
+ } as ChatMessage;
987
+
988
+ act(() => {
989
+ useChatStore.setState({
990
+ activeId: 'anbccfdd',
991
+ messagesMap: { [messageMapKey('anbccfdd')]: [assistantMessage] },
992
+ refreshMessages: vi.fn(),
993
+ });
994
+ });
995
+
996
+ await act(async () => {
997
+ await result.current.internal_addToolToAssistantMessage(messageId, {
998
+ identifier,
999
+ arguments: '{"oldKey":"oldValue"}',
1000
+ id: 'newId',
1001
+ apiName: 'test',
1002
+ type: 'default',
1003
+ });
1004
+ });
1005
+
1006
+ expect(refreshToUpdateMessageToolsSpy).toHaveBeenCalledWith(messageId);
1007
+ });
1008
+ });
967
1009
  });
@@ -43,6 +43,13 @@ export interface ChatPluginAction {
43
43
  updatePluginState: (id: string, value: any) => Promise<void>;
44
44
  updatePluginArguments: <T = any>(id: string, value: T) => Promise<void>;
45
45
 
46
+ internal_addToolToAssistantMessage: (id: string, tool: ChatToolPayload) => Promise<void>;
47
+ internal_removeToolToAssistantMessage: (id: string, tool_call_id?: string) => Promise<void>;
48
+ /**
49
+ * use the optimistic update value to update the message tools to database
50
+ */
51
+ internal_refreshToUpdateMessageTools: (id: string) => Promise<void>;
52
+
46
53
  internal_callPluginApi: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
47
54
  internal_invokeDifferentTypePlugin: (id: string, payload: ChatToolPayload) => Promise<any>;
48
55
  internal_togglePluginApiCalling: (
@@ -281,6 +288,45 @@ export const chatPlugin: StateCreator<
281
288
  await refreshMessages();
282
289
  },
283
290
 
291
+ internal_addToolToAssistantMessage: async (id, tool) => {
292
+ const assistantMessage = chatSelectors.getMessageById(id)(get());
293
+ if (!assistantMessage) return;
294
+
295
+ const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
296
+ internal_dispatchMessage({
297
+ type: 'addMessageTool',
298
+ value: tool,
299
+ id: assistantMessage.id,
300
+ });
301
+
302
+ await internal_refreshToUpdateMessageTools(id);
303
+ },
304
+
305
+ internal_removeToolToAssistantMessage: async (id, tool_call_id) => {
306
+ const message = chatSelectors.getMessageById(id)(get());
307
+ if (!message || !tool_call_id) return;
308
+
309
+ const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
310
+
311
+ // optimistic update
312
+ internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id });
313
+
314
+ // update the message tools
315
+ await internal_refreshToUpdateMessageTools(id);
316
+ },
317
+ internal_refreshToUpdateMessageTools: async (id) => {
318
+ const message = chatSelectors.getMessageById(id)(get());
319
+ if (!message || !message.tools) return;
320
+
321
+ const { internal_toggleMessageLoading, refreshMessages } = get();
322
+
323
+ internal_toggleMessageLoading(true, id);
324
+ await messageService.updateMessage(id, { tools: message.tools });
325
+ internal_toggleMessageLoading(false, id);
326
+
327
+ await refreshMessages();
328
+ },
329
+
284
330
  internal_callPluginApi: async (id, payload) => {
285
331
  const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
286
332
  get();
@@ -48,7 +48,7 @@ describe('chatDockSlice', () => {
48
48
  const { result } = renderHook(() => useChatStore());
49
49
 
50
50
  act(() => {
51
- result.current.toggleDock(true);
51
+ result.current.togglePortal(true);
52
52
  });
53
53
 
54
54
  expect(result.current.showPortal).toBe(true);
@@ -72,13 +72,13 @@ describe('chatDockSlice', () => {
72
72
  expect(result.current.showPortal).toBe(false);
73
73
 
74
74
  act(() => {
75
- result.current.toggleDock();
75
+ result.current.togglePortal();
76
76
  });
77
77
 
78
78
  expect(result.current.showPortal).toBe(true);
79
79
 
80
80
  act(() => {
81
- result.current.toggleDock();
81
+ result.current.togglePortal();
82
82
  });
83
83
 
84
84
  expect(result.current.showPortal).toBe(false);
@@ -88,19 +88,19 @@ describe('chatDockSlice', () => {
88
88
  const { result } = renderHook(() => useChatStore());
89
89
 
90
90
  act(() => {
91
- result.current.toggleDock(true);
91
+ result.current.togglePortal(true);
92
92
  });
93
93
 
94
94
  expect(result.current.showPortal).toBe(true);
95
95
 
96
96
  act(() => {
97
- result.current.toggleDock(false);
97
+ result.current.togglePortal(false);
98
98
  });
99
99
 
100
100
  expect(result.current.showPortal).toBe(false);
101
101
 
102
102
  act(() => {
103
- result.current.toggleDock(true);
103
+ result.current.togglePortal(true);
104
104
  });
105
105
 
106
106
  expect(result.current.showPortal).toBe(true);
@@ -5,7 +5,7 @@ import { ChatStore } from '@/store/chat/store';
5
5
  export interface ChatPortalAction {
6
6
  closeToolUI: () => void;
7
7
  openToolUI: (messageId: string, identifier: string) => void;
8
- toggleDock: (open?: boolean) => void;
8
+ togglePortal: (open?: boolean) => void;
9
9
  }
10
10
 
11
11
  export const chatPortalSlice: StateCreator<
@@ -19,12 +19,12 @@ export const chatPortalSlice: StateCreator<
19
19
  },
20
20
  openToolUI: (id, identifier) => {
21
21
  if (!get().showPortal) {
22
- get().toggleDock(true);
22
+ get().togglePortal(true);
23
23
  }
24
24
 
25
25
  set({ portalToolMessage: { id, identifier } }, false, 'openToolUI');
26
26
  },
27
- toggleDock: (open) => {
27
+ togglePortal: (open) => {
28
28
  const showInspector = open === undefined ? !get().showPortal : open;
29
29
  set({ showPortal: showInspector }, false, 'toggleInspector');
30
30
  },
@@ -33,6 +33,7 @@ vi.mock('@/services/topic', () => ({
33
33
  vi.mock('@/services/message', () => ({
34
34
  messageService: {
35
35
  removeMessages: vi.fn(),
36
+ removeMessagesByAssistant: vi.fn(),
36
37
  getMessages: vi.fn(),
37
38
  },
38
39
  }));
@@ -351,7 +352,7 @@ describe('topic action', () => {
351
352
  await result.current.removeTopic(topicId);
352
353
  });
353
354
 
354
- expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
355
+ expect(messageService.removeMessagesByAssistant).toHaveBeenCalledWith(activeId, topicId);
355
356
  expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
356
357
  expect(refreshTopicSpy).toHaveBeenCalled();
357
358
  expect(switchTopicSpy).toHaveBeenCalled();
@@ -372,7 +373,7 @@ describe('topic action', () => {
372
373
  await result.current.removeTopic(topicId);
373
374
  });
374
375
 
375
- expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
376
+ expect(messageService.removeMessagesByAssistant).toHaveBeenCalledWith(activeId, topicId);
376
377
  expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
377
378
  expect(refreshTopicSpy).toHaveBeenCalled();
378
379
  expect(switchTopicSpy).not.toHaveBeenCalled();
@@ -249,7 +249,7 @@ export const chatTopic: StateCreator<
249
249
 
250
250
  // remove messages in the topic
251
251
  // TODO: Need to remove because server service don't need to call it
252
- await messageService.removeMessages(activeId, id);
252
+ await messageService.removeMessagesByAssistant(activeId, id);
253
253
 
254
254
  // remove topic
255
255
  await topicService.removeTopic(id);
@@ -69,6 +69,19 @@ describe('createPreferenceSlice', () => {
69
69
  });
70
70
  });
71
71
 
72
+ describe('toggleMobilePortal', () => {
73
+ it('should toggle mobile topic', () => {
74
+ const { result } = renderHook(() => useGlobalStore());
75
+
76
+ act(() => {
77
+ useGlobalStore.setState({ isStatusInit: true });
78
+ result.current.toggleMobilePortal();
79
+ });
80
+
81
+ expect(result.current.status.mobileShowPortal).toBe(true);
82
+ });
83
+ });
84
+
72
85
  describe('toggleSystemRole', () => {
73
86
  it('should toggle system role', () => {
74
87
  const { result } = renderHook(() => useGlobalStore());
@@ -24,6 +24,7 @@ export interface GlobalStoreAction {
24
24
  switchBackToChat: (sessionId?: string) => void;
25
25
  toggleChatSideBar: (visible?: boolean) => void;
26
26
  toggleExpandSessionGroup: (id: string, expand: boolean) => void;
27
+ toggleMobilePortal: (visible?: boolean) => void;
27
28
  toggleMobileTopic: (visible?: boolean) => void;
28
29
  toggleSystemRole: (visible?: boolean) => void;
29
30
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
@@ -59,6 +60,12 @@ export const globalActionSlice: StateCreator<
59
60
  });
60
61
  get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup });
61
62
  },
63
+ toggleMobilePortal: (newValue) => {
64
+ const mobileShowPortal =
65
+ typeof newValue === 'boolean' ? newValue : !get().status.mobileShowPortal;
66
+
67
+ get().updateSystemStatus({ mobileShowPortal }, n('toggleMobilePortal', newValue));
68
+ },
62
69
  toggleMobileTopic: (newValue) => {
63
70
  const mobileShowTopic =
64
71
  typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
@@ -34,6 +34,7 @@ export interface SystemStatus {
34
34
  expandSessionGroupKeys: string[];
35
35
  hidePWAInstaller?: boolean;
36
36
  inputHeight: number;
37
+ mobileShowPortal?: boolean;
37
38
  mobileShowTopic?: boolean;
38
39
  sessionsWidth: number;
39
40
  showChatSideBar?: boolean;
@@ -7,6 +7,7 @@ const sessionGroupKeys = (s: GlobalStore): string[] =>
7
7
 
8
8
  const showSystemRole = (s: GlobalStore) => s.status.showSystemRole;
9
9
  const mobileShowTopic = (s: GlobalStore) => s.status.mobileShowTopic;
10
+ const mobileShowPortal = (s: GlobalStore) => s.status.mobileShowPortal;
10
11
  const showChatSideBar = (s: GlobalStore) => s.status.showChatSideBar;
11
12
  const showSessionPanel = (s: GlobalStore) => s.status.showSessionPanel;
12
13
  const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
@@ -17,6 +18,7 @@ const inputHeight = (s: GlobalStore) => s.status.inputHeight;
17
18
  export const systemStatusSelectors = {
18
19
  hidePWAInstaller,
19
20
  inputHeight,
21
+ mobileShowPortal,
20
22
  mobileShowTopic,
21
23
  sessionGroupKeys,
22
24
  sessionWidth,