@lobehub/chat 1.2.6 → 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.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@portal/features/ArtifactUI/Footer.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +1 -1
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/PortalModal.tsx +35 -0
- package/src/app/(main)/chat/(workspace)/_layout/Mobile/index.tsx +3 -1
- package/src/database/client/models/__tests__/message.test.ts +13 -0
- package/src/database/client/models/message.ts +4 -0
- package/src/database/server/models/__tests__/message.test.ts +103 -0
- package/src/database/server/models/message.ts +13 -6
- package/src/features/Conversation/Actions/Tool.tsx +12 -6
- package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +25 -19
- package/src/features/Conversation/Messages/Tool/Inspector/style.ts +9 -0
- package/src/server/routers/lambda/message.ts +7 -1
- package/src/services/message/client.test.ts +16 -2
- package/src/services/message/client.ts +5 -1
- package/src/services/message/server.ts +7 -2
- package/src/services/message/type.ts +2 -1
- package/src/store/chat/slices/message/action.test.ts +144 -0
- package/src/store/chat/slices/message/action.ts +111 -64
- package/src/store/chat/slices/message/reducer.test.ts +200 -1
- package/src/store/chat/slices/message/reducer.ts +62 -2
- package/src/store/chat/slices/plugin/action.test.ts +42 -0
- package/src/store/chat/slices/plugin/action.ts +46 -0
- package/src/store/chat/slices/portal/action.test.ts +6 -6
- package/src/store/chat/slices/portal/action.ts +3 -3
- package/src/store/chat/slices/topic/action.test.ts +3 -2
- package/src/store/chat/slices/topic/action.ts +1 -1
- package/src/store/global/action.test.ts +13 -0
- package/src/store/global/action.ts +7 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/global/selectors.ts +2 -0
|
@@ -30,6 +30,7 @@ vi.mock('@/services/message', () => ({
|
|
|
30
30
|
getMessages: vi.fn(),
|
|
31
31
|
updateMessageError: vi.fn(),
|
|
32
32
|
removeMessage: vi.fn(),
|
|
33
|
+
removeMessagesByAssistant: vi.fn(),
|
|
33
34
|
removeMessages: vi.fn(() => Promise.resolve()),
|
|
34
35
|
createMessage: vi.fn(() => Promise.resolve('new-message-id')),
|
|
35
36
|
updateMessage: vi.fn(),
|
|
@@ -147,6 +148,124 @@ describe('chatMessage actions', () => {
|
|
|
147
148
|
expect(deleteSpy).toHaveBeenCalledWith(messageId);
|
|
148
149
|
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
149
150
|
});
|
|
151
|
+
|
|
152
|
+
it('deleteMessage should remove messages with tools', async () => {
|
|
153
|
+
const { result } = renderHook(() => useChatStore());
|
|
154
|
+
const messageId = 'message-id';
|
|
155
|
+
const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
|
|
156
|
+
|
|
157
|
+
act(() => {
|
|
158
|
+
useChatStore.setState({
|
|
159
|
+
activeId: 'session-id',
|
|
160
|
+
activeTopicId: undefined,
|
|
161
|
+
messagesMap: {
|
|
162
|
+
[messageMapKey('session-id')]: [
|
|
163
|
+
{ id: messageId, tools: [{ id: 'tool1' }, { id: 'tool2' }] } as ChatMessage,
|
|
164
|
+
{ id: '2', tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
|
|
165
|
+
{ id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
await act(async () => {
|
|
171
|
+
await result.current.deleteMessage(messageId);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(removeMessagesSpy).toHaveBeenCalledWith([messageId, '2', '3']);
|
|
175
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('deleteToolMessage', () => {
|
|
180
|
+
it('deleteMessage should remove a message by id', async () => {
|
|
181
|
+
const { result } = renderHook(() => useChatStore());
|
|
182
|
+
const messageId = 'message-id';
|
|
183
|
+
const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
|
|
184
|
+
const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
|
|
185
|
+
|
|
186
|
+
act(() => {
|
|
187
|
+
useChatStore.setState({
|
|
188
|
+
activeId: 'session-id',
|
|
189
|
+
activeTopicId: undefined,
|
|
190
|
+
messagesMap: {
|
|
191
|
+
[messageMapKey('session-id')]: [
|
|
192
|
+
{
|
|
193
|
+
id: messageId,
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
tools: [{ id: 'tool1' }, { id: 'tool2' }],
|
|
196
|
+
} as ChatMessage,
|
|
197
|
+
{ id: '2', parentId: messageId, tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
|
|
198
|
+
{ id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await result.current.deleteToolMessage('2');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(removeMessageSpy).toHaveBeenCalled();
|
|
208
|
+
expect(updateMessageSpy).toHaveBeenCalledWith('message-id', {
|
|
209
|
+
tools: [{ id: 'tool2' }],
|
|
210
|
+
});
|
|
211
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('delAndRegenerateMessage', () => {
|
|
216
|
+
it('should remove a message and create a new message', async () => {
|
|
217
|
+
const { result } = renderHook(() => useChatStore());
|
|
218
|
+
const messageId = 'message-id';
|
|
219
|
+
const deleteMessageSpy = vi.spyOn(result.current, 'deleteMessage');
|
|
220
|
+
const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
|
|
221
|
+
|
|
222
|
+
act(() => {
|
|
223
|
+
useChatStore.setState({
|
|
224
|
+
activeId: 'session-id',
|
|
225
|
+
activeTopicId: undefined,
|
|
226
|
+
messagesMap: {
|
|
227
|
+
[messageMapKey('session-id')]: [
|
|
228
|
+
{ id: messageId, tools: [{ id: 'tool1' }, { id: 'tool2' }] } as ChatMessage,
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
await act(async () => {
|
|
234
|
+
await result.current.delAndRegenerateMessage(messageId);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(deleteMessageSpy).toHaveBeenCalledWith(messageId);
|
|
238
|
+
expect(resendMessageSpy).toHaveBeenCalled();
|
|
239
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('regenerateMessage', () => {
|
|
243
|
+
it('should create a new message', async () => {
|
|
244
|
+
const { result } = renderHook(() => useChatStore());
|
|
245
|
+
const messageId = 'message-id';
|
|
246
|
+
const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
|
|
247
|
+
|
|
248
|
+
act(() => {
|
|
249
|
+
useChatStore.setState({
|
|
250
|
+
activeId: 'session-id',
|
|
251
|
+
activeTopicId: undefined,
|
|
252
|
+
messagesMap: {
|
|
253
|
+
[messageMapKey('session-id')]: [
|
|
254
|
+
{
|
|
255
|
+
id: messageId,
|
|
256
|
+
tools: [{ id: 'tool1' }, { id: 'tool2' }],
|
|
257
|
+
traceId: 'abc',
|
|
258
|
+
} as ChatMessage,
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
await act(async () => {
|
|
264
|
+
await result.current.regenerateMessage(messageId);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(resendMessageSpy).toHaveBeenCalledWith(messageId, 'abc');
|
|
268
|
+
});
|
|
150
269
|
});
|
|
151
270
|
|
|
152
271
|
describe('clearAllMessages', () => {
|
|
@@ -1144,6 +1263,31 @@ describe('chatMessage actions', () => {
|
|
|
1144
1263
|
});
|
|
1145
1264
|
});
|
|
1146
1265
|
|
|
1266
|
+
describe('internal_toggleToolCallingStreaming action', () => {
|
|
1267
|
+
it('should add message id to messageLoadingIds when loading is true', () => {
|
|
1268
|
+
const { result } = renderHook(() => useChatStore());
|
|
1269
|
+
const messageId = 'message-id';
|
|
1270
|
+
|
|
1271
|
+
act(() => {
|
|
1272
|
+
result.current.internal_toggleToolCallingStreaming(messageId, [true]);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
expect(result.current.toolCallingStreamIds[messageId]).toEqual([true]);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it('should remove message id from messageLoadingIds when loading is false', () => {
|
|
1279
|
+
const { result } = renderHook(() => useChatStore());
|
|
1280
|
+
const messageId = 'ddd-id';
|
|
1281
|
+
|
|
1282
|
+
act(() => {
|
|
1283
|
+
result.current.internal_toggleToolCallingStreaming(messageId, [true]);
|
|
1284
|
+
result.current.internal_toggleToolCallingStreaming(messageId, undefined);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
expect(result.current.toolCallingStreamIds[messageId]).toBeUndefined();
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1147
1291
|
describe('stopGenerateMessage', () => {
|
|
1148
1292
|
it('should return early if abortController is undefined', () => {
|
|
1149
1293
|
act(() => {
|
|
@@ -66,6 +66,7 @@ export interface ChatMessageAction {
|
|
|
66
66
|
*/
|
|
67
67
|
clearMessage: () => Promise<void>;
|
|
68
68
|
deleteMessage: (id: string) => Promise<void>;
|
|
69
|
+
deleteToolMessage: (id: string) => Promise<void>;
|
|
69
70
|
delAndRegenerateMessage: (id: string) => Promise<void>;
|
|
70
71
|
clearAllMessages: () => Promise<void>;
|
|
71
72
|
// update
|
|
@@ -81,19 +82,7 @@ export interface ChatMessageAction {
|
|
|
81
82
|
// ========= ↓ Internal Method ↓ ========== //
|
|
82
83
|
// ========================================== //
|
|
83
84
|
// ========================================== //
|
|
84
|
-
|
|
85
|
-
loading: boolean,
|
|
86
|
-
id?: string,
|
|
87
|
-
action?: string,
|
|
88
|
-
) => AbortController | undefined;
|
|
89
|
-
internal_toggleLoadingArrays: (
|
|
90
|
-
key: keyof ChatStoreState,
|
|
91
|
-
loading: boolean,
|
|
92
|
-
id?: string,
|
|
93
|
-
action?: string,
|
|
94
|
-
) => AbortController | undefined;
|
|
95
|
-
internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
|
|
96
|
-
internal_toggleMessageLoading: (loading: boolean, id: string) => void;
|
|
85
|
+
|
|
97
86
|
/**
|
|
98
87
|
* update message at the frontend point
|
|
99
88
|
* this method will not update messages to database
|
|
@@ -108,9 +97,7 @@ export interface ChatMessageAction {
|
|
|
108
97
|
params?: ProcessMessageParams,
|
|
109
98
|
) => Promise<void>;
|
|
110
99
|
/**
|
|
111
|
-
*
|
|
112
|
-
* @param messages - 聊天消息数组
|
|
113
|
-
* @param options - 获取 SSE 选项
|
|
100
|
+
* the method to fetch the AI message
|
|
114
101
|
*/
|
|
115
102
|
internal_fetchAIChatMessage: (
|
|
116
103
|
messages: ChatMessage[],
|
|
@@ -122,24 +109,66 @@ export interface ChatMessageAction {
|
|
|
122
109
|
}>;
|
|
123
110
|
|
|
124
111
|
/**
|
|
112
|
+
* update the message content with optimistic update
|
|
125
113
|
* a method used by other action
|
|
126
|
-
* @param id
|
|
127
|
-
* @param content
|
|
128
114
|
*/
|
|
129
115
|
internal_updateMessageContent: (
|
|
130
116
|
id: string,
|
|
131
117
|
content: string,
|
|
132
118
|
toolCalls?: MessageToolCall[],
|
|
133
119
|
) => Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* update the message error with optimistic update
|
|
122
|
+
*/
|
|
134
123
|
internal_updateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* create a message with optimistic update
|
|
126
|
+
*/
|
|
135
127
|
internal_createMessage: (
|
|
136
128
|
params: CreateMessageParams,
|
|
137
129
|
context?: { tempMessageId?: string; skipRefresh?: boolean },
|
|
138
130
|
) => Promise<string>;
|
|
131
|
+
/**
|
|
132
|
+
* create a temp message for optimistic update
|
|
133
|
+
* otherwise the message will be too slow to show
|
|
134
|
+
*/
|
|
139
135
|
internal_createTmpMessage: (params: CreateMessageParams) => string;
|
|
140
|
-
|
|
136
|
+
/**
|
|
137
|
+
* delete the message content with optimistic update
|
|
138
|
+
*/
|
|
139
|
+
internal_deleteMessage: (id: string) => Promise<void>;
|
|
141
140
|
internal_resendMessage: (id: string, traceId?: string) => Promise<void>;
|
|
141
|
+
|
|
142
|
+
internal_fetchMessages: () => Promise<void>;
|
|
142
143
|
internal_traceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* method to toggle message create loading state
|
|
147
|
+
* the AI message status is creating -> generating
|
|
148
|
+
* other message role like user and tool , only this method need to be called
|
|
149
|
+
*/
|
|
150
|
+
internal_toggleMessageLoading: (loading: boolean, id: string) => void;
|
|
151
|
+
/**
|
|
152
|
+
* method to toggle ai message generating loading
|
|
153
|
+
*/
|
|
154
|
+
internal_toggleChatLoading: (
|
|
155
|
+
loading: boolean,
|
|
156
|
+
id?: string,
|
|
157
|
+
action?: string,
|
|
158
|
+
) => AbortController | undefined;
|
|
159
|
+
/**
|
|
160
|
+
* method to toggle the tool calling loading state
|
|
161
|
+
*/
|
|
162
|
+
internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
|
|
163
|
+
/**
|
|
164
|
+
* helper to toggle the loading state of the array,used by these three toggleXXXLoading
|
|
165
|
+
*/
|
|
166
|
+
internal_toggleLoadingArrays: (
|
|
167
|
+
key: keyof ChatStoreState,
|
|
168
|
+
loading: boolean,
|
|
169
|
+
id?: string,
|
|
170
|
+
action?: string,
|
|
171
|
+
) => AbortController | undefined;
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
|
|
@@ -155,30 +184,42 @@ export const chatMessage: StateCreator<
|
|
|
155
184
|
const message = chatSelectors.getMessageById(id)(get());
|
|
156
185
|
if (!message) return;
|
|
157
186
|
|
|
158
|
-
|
|
159
|
-
get().internal_dispatchMessage({ type: 'deleteMessage', id });
|
|
160
|
-
await messageService.removeMessage(id);
|
|
161
|
-
};
|
|
187
|
+
let ids = [message.id];
|
|
162
188
|
|
|
163
189
|
// if the message is a tool calls, then delete all the related messages
|
|
164
|
-
// TODO: maybe we need to delete it in the DB?
|
|
165
190
|
if (message.tools) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.filter((m) => m.tool_call_id === tool.id);
|
|
171
|
-
|
|
172
|
-
return messages.map((m) => m.id);
|
|
173
|
-
})
|
|
174
|
-
.map((i) => deleteFn(i));
|
|
191
|
+
const toolMessageIds = message.tools.flatMap((tool) => {
|
|
192
|
+
const messages = chatSelectors
|
|
193
|
+
.currentChats(get())
|
|
194
|
+
.filter((m) => m.tool_call_id === tool.id);
|
|
175
195
|
|
|
176
|
-
|
|
196
|
+
return messages.map((m) => m.id);
|
|
197
|
+
});
|
|
198
|
+
ids = ids.concat(toolMessageIds);
|
|
177
199
|
}
|
|
178
200
|
|
|
179
|
-
|
|
201
|
+
get().internal_dispatchMessage({ type: 'deleteMessages', ids });
|
|
202
|
+
await messageService.removeMessages(ids);
|
|
180
203
|
await get().refreshMessages();
|
|
181
204
|
},
|
|
205
|
+
|
|
206
|
+
deleteToolMessage: async (id) => {
|
|
207
|
+
const message = chatSelectors.getMessageById(id)(get());
|
|
208
|
+
if (!message || message.role !== 'tool') return;
|
|
209
|
+
|
|
210
|
+
const removeToolInAssistantMessage = async () => {
|
|
211
|
+
if (!message.parentId) return;
|
|
212
|
+
await get().internal_removeToolToAssistantMessage(message.parentId, message.tool_call_id);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await Promise.all([
|
|
216
|
+
// 1. remove tool message
|
|
217
|
+
get().internal_deleteMessage(id),
|
|
218
|
+
// 2. remove the tool item in the assistant tools
|
|
219
|
+
removeToolInAssistantMessage(),
|
|
220
|
+
]);
|
|
221
|
+
},
|
|
222
|
+
|
|
182
223
|
delAndRegenerateMessage: async (id) => {
|
|
183
224
|
const traceId = chatSelectors.getTraceIdByMessageId(id)(get());
|
|
184
225
|
get().internal_resendMessage(id, traceId);
|
|
@@ -197,7 +238,7 @@ export const chatMessage: StateCreator<
|
|
|
197
238
|
clearMessage: async () => {
|
|
198
239
|
const { activeId, activeTopicId, refreshMessages, refreshTopic, switchTopic } = get();
|
|
199
240
|
|
|
200
|
-
await messageService.
|
|
241
|
+
await messageService.removeMessagesByAssistant(activeId, activeTopicId);
|
|
201
242
|
|
|
202
243
|
if (activeTopicId) {
|
|
203
244
|
await topicService.removeTopic(activeTopicId);
|
|
@@ -581,34 +622,7 @@ export const chatMessage: StateCreator<
|
|
|
581
622
|
traceId: msgTraceId,
|
|
582
623
|
};
|
|
583
624
|
},
|
|
584
|
-
internal_toggleChatLoading: (loading, id, action) => {
|
|
585
|
-
return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
|
|
586
|
-
},
|
|
587
|
-
internal_toggleMessageLoading: (loading, id) => {
|
|
588
|
-
set(
|
|
589
|
-
{
|
|
590
|
-
messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
|
|
591
|
-
},
|
|
592
|
-
false,
|
|
593
|
-
'internal_toggleMessageLoading',
|
|
594
|
-
);
|
|
595
|
-
},
|
|
596
|
-
internal_toggleToolCallingStreaming: (id, streaming) => {
|
|
597
|
-
set(
|
|
598
|
-
{
|
|
599
|
-
toolCallingStreamIds: produce(get().toolCallingStreamIds, (draft) => {
|
|
600
|
-
if (!!streaming) {
|
|
601
|
-
draft[id] = streaming;
|
|
602
|
-
} else {
|
|
603
|
-
delete draft[id];
|
|
604
|
-
}
|
|
605
|
-
}),
|
|
606
|
-
},
|
|
607
625
|
|
|
608
|
-
false,
|
|
609
|
-
'toggleToolCallingStreaming',
|
|
610
|
-
);
|
|
611
|
-
},
|
|
612
626
|
internal_resendMessage: async (messageId, traceId) => {
|
|
613
627
|
// 1. 构造所有相关的历史记录
|
|
614
628
|
const chats = chatSelectors.currentChats(get());
|
|
@@ -715,7 +729,11 @@ export const chatMessage: StateCreator<
|
|
|
715
729
|
|
|
716
730
|
return tempId;
|
|
717
731
|
},
|
|
718
|
-
|
|
732
|
+
internal_deleteMessage: async (id: string) => {
|
|
733
|
+
get().internal_dispatchMessage({ type: 'deleteMessage', id });
|
|
734
|
+
await messageService.removeMessage(id);
|
|
735
|
+
await get().refreshMessages();
|
|
736
|
+
},
|
|
719
737
|
internal_traceMessage: async (id, payload) => {
|
|
720
738
|
// tracing the diff of update
|
|
721
739
|
const message = chatSelectors.getMessageById(id)(get());
|
|
@@ -731,6 +749,35 @@ export const chatMessage: StateCreator<
|
|
|
731
749
|
}
|
|
732
750
|
},
|
|
733
751
|
|
|
752
|
+
// ----- Loading ------- //
|
|
753
|
+
internal_toggleMessageLoading: (loading, id) => {
|
|
754
|
+
set(
|
|
755
|
+
{
|
|
756
|
+
messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
|
|
757
|
+
},
|
|
758
|
+
false,
|
|
759
|
+
'internal_toggleMessageLoading',
|
|
760
|
+
);
|
|
761
|
+
},
|
|
762
|
+
internal_toggleChatLoading: (loading, id, action) => {
|
|
763
|
+
return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
|
|
764
|
+
},
|
|
765
|
+
internal_toggleToolCallingStreaming: (id, streaming) => {
|
|
766
|
+
set(
|
|
767
|
+
{
|
|
768
|
+
toolCallingStreamIds: produce(get().toolCallingStreamIds, (draft) => {
|
|
769
|
+
if (!!streaming) {
|
|
770
|
+
draft[id] = streaming;
|
|
771
|
+
} else {
|
|
772
|
+
delete draft[id];
|
|
773
|
+
}
|
|
774
|
+
}),
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
false,
|
|
778
|
+
'toggleToolCallingStreaming',
|
|
779
|
+
);
|
|
780
|
+
},
|
|
734
781
|
internal_toggleLoadingArrays: (key, loading, id, action) => {
|
|
735
782
|
if (loading) {
|
|
736
783
|
window.addEventListener('beforeunload', preventLeavingFn);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatMessage } from '@/types/message';
|
|
1
|
+
import { ChatMessage, ChatToolPayload } from '@/types/message';
|
|
2
2
|
|
|
3
3
|
import { MessageDispatch, messagesReducer } from './reducer';
|
|
4
4
|
|
|
@@ -97,6 +97,35 @@ describe('messagesReducer', () => {
|
|
|
97
97
|
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
it('should update the extra field of a message if extra exist', () => {
|
|
101
|
+
const payload: MessageDispatch = {
|
|
102
|
+
type: 'updateMessageExtra',
|
|
103
|
+
id: 'data',
|
|
104
|
+
key: 'abc',
|
|
105
|
+
value: '2',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const newState = messagesReducer(
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
id: 'data',
|
|
112
|
+
content: 'Hello World',
|
|
113
|
+
createdAt: 1629264000000,
|
|
114
|
+
updatedAt: 1629264000000,
|
|
115
|
+
role: 'user',
|
|
116
|
+
meta: {},
|
|
117
|
+
extra: { abc: '1' },
|
|
118
|
+
} as ChatMessage,
|
|
119
|
+
...initialState,
|
|
120
|
+
],
|
|
121
|
+
payload,
|
|
122
|
+
);
|
|
123
|
+
const updatedMessage = newState.find((m) => m.id === 'data');
|
|
124
|
+
|
|
125
|
+
expect(updatedMessage?.extra).toEqual({ abc: '2' });
|
|
126
|
+
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
|
|
127
|
+
});
|
|
128
|
+
|
|
100
129
|
it('should not modify state if message is not found', () => {
|
|
101
130
|
const payload: MessageDispatch = {
|
|
102
131
|
type: 'updateMessageExtra',
|
|
@@ -255,6 +284,151 @@ describe('messagesReducer', () => {
|
|
|
255
284
|
});
|
|
256
285
|
});
|
|
257
286
|
|
|
287
|
+
describe('addMessageTool', () => {
|
|
288
|
+
it('should add a tool to the specified assistant message if it dont have tools', () => {
|
|
289
|
+
const messageId = '1';
|
|
290
|
+
const toolPayload: ChatToolPayload = {
|
|
291
|
+
id: 'tc_1',
|
|
292
|
+
type: 'default',
|
|
293
|
+
identifier: 'tool1',
|
|
294
|
+
apiName: 'testFunction',
|
|
295
|
+
arguments: '{"arg1": "value1"}',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const payload: MessageDispatch = {
|
|
299
|
+
type: 'addMessageTool',
|
|
300
|
+
id: messageId,
|
|
301
|
+
value: toolPayload,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const newState = messagesReducer(
|
|
305
|
+
[...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
|
|
306
|
+
payload,
|
|
307
|
+
);
|
|
308
|
+
const updatedMessage = newState.find((m) => m.id === messageId);
|
|
309
|
+
|
|
310
|
+
expect(updatedMessage).not.toBeUndefined();
|
|
311
|
+
expect(updatedMessage?.tools).toHaveLength(1);
|
|
312
|
+
expect(updatedMessage?.tools?.[0]).toEqual(toolPayload);
|
|
313
|
+
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should add a tool to the specified assistant message', () => {
|
|
317
|
+
const messageId = 'message2';
|
|
318
|
+
const toolPayload: ChatToolPayload = {
|
|
319
|
+
id: 'tc_1',
|
|
320
|
+
type: 'default',
|
|
321
|
+
identifier: 'tool1',
|
|
322
|
+
apiName: 'testFunction',
|
|
323
|
+
arguments: '{"arg1": "value1"}',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const payload: MessageDispatch = {
|
|
327
|
+
type: 'addMessageTool',
|
|
328
|
+
id: messageId,
|
|
329
|
+
value: toolPayload,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const newState = messagesReducer(
|
|
333
|
+
[...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
|
|
334
|
+
payload,
|
|
335
|
+
);
|
|
336
|
+
const updatedMessage = newState.find((m) => m.id === messageId);
|
|
337
|
+
|
|
338
|
+
expect(updatedMessage).not.toBeUndefined();
|
|
339
|
+
expect(updatedMessage?.tools).toHaveLength(2);
|
|
340
|
+
expect(updatedMessage?.tools?.[1]).toEqual(toolPayload);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should not modify the state if the message is not found', () => {
|
|
344
|
+
const toolPayload: ChatToolPayload = {
|
|
345
|
+
id: 'tc_1',
|
|
346
|
+
type: 'default',
|
|
347
|
+
identifier: 'tool1',
|
|
348
|
+
apiName: 'testFunction',
|
|
349
|
+
arguments: '{"arg1": "value1"}',
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const payload: MessageDispatch = {
|
|
353
|
+
type: 'addMessageTool',
|
|
354
|
+
id: 'nonexistentMessage',
|
|
355
|
+
value: toolPayload,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const newState = messagesReducer(initialState, payload);
|
|
359
|
+
expect(newState).toEqual(initialState);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should not add a tool if the message is not an assistant message', () => {
|
|
363
|
+
const toolPayload: ChatToolPayload = {
|
|
364
|
+
id: 'tc_1',
|
|
365
|
+
type: 'default',
|
|
366
|
+
identifier: 'tool1',
|
|
367
|
+
apiName: 'testFunction',
|
|
368
|
+
arguments: '{"arg1": "value1"}',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const payload: MessageDispatch = {
|
|
372
|
+
type: 'addMessageTool',
|
|
373
|
+
id: 'message1', // This is a user message
|
|
374
|
+
value: toolPayload,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const newState = messagesReducer(initialState, payload);
|
|
378
|
+
expect(newState).toEqual(initialState);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('deleteMessageTool', () => {
|
|
383
|
+
it('should delete the specified tool from the message', () => {
|
|
384
|
+
const payload: MessageDispatch = {
|
|
385
|
+
type: 'deleteMessageTool',
|
|
386
|
+
id: 'message2',
|
|
387
|
+
tool_call_id: 'abc',
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const newState = messagesReducer(initialState, payload);
|
|
391
|
+
const updatedMessage = newState.find((m) => m.id === 'message2');
|
|
392
|
+
|
|
393
|
+
expect(updatedMessage).not.toBeUndefined();
|
|
394
|
+
expect(updatedMessage?.tools).toHaveLength(0);
|
|
395
|
+
expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should not modify the state if the message is not found', () => {
|
|
399
|
+
const payload: MessageDispatch = {
|
|
400
|
+
type: 'deleteMessageTool',
|
|
401
|
+
id: 'nonexistentMessage',
|
|
402
|
+
tool_call_id: 'tool1',
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const newState = messagesReducer(initialState, payload);
|
|
406
|
+
expect(newState).toEqual(initialState);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should not modify the state if the tool is not found', () => {
|
|
410
|
+
const payload: MessageDispatch = {
|
|
411
|
+
type: 'deleteMessageTool',
|
|
412
|
+
id: 'message1',
|
|
413
|
+
tool_call_id: 'nonexistentTool',
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const newState = messagesReducer(initialState, payload);
|
|
417
|
+
expect(newState).toEqual(initialState);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should not delete a tool if the message is not an assistant message', () => {
|
|
421
|
+
const payload: MessageDispatch = {
|
|
422
|
+
type: 'deleteMessageTool',
|
|
423
|
+
id: 'message1', // This is a user message
|
|
424
|
+
tool_call_id: 'tool1',
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const newState = messagesReducer(initialState, payload);
|
|
428
|
+
expect(newState).toEqual(initialState);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
258
432
|
describe('createMessage', () => {
|
|
259
433
|
it('should add a new message to the state', () => {
|
|
260
434
|
const payload: MessageDispatch = {
|
|
@@ -303,4 +477,29 @@ describe('messagesReducer', () => {
|
|
|
303
477
|
expect(newState).toEqual(initialState);
|
|
304
478
|
});
|
|
305
479
|
});
|
|
480
|
+
|
|
481
|
+
describe('deleteMessages', () => {
|
|
482
|
+
it('should remove 2 messages from the state', () => {
|
|
483
|
+
const payload: MessageDispatch = {
|
|
484
|
+
type: 'deleteMessages',
|
|
485
|
+
ids: ['message1', 'message2'],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const newState = messagesReducer(initialState, payload);
|
|
489
|
+
|
|
490
|
+
expect(newState.length).toBe(0);
|
|
491
|
+
expect(newState.find((m) => m.id === 'message1')).toBeUndefined();
|
|
492
|
+
expect(newState.find((m) => m.id === 'message2')).toBeUndefined();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should not modify state if message to delete is not found', () => {
|
|
496
|
+
const payload: MessageDispatch = {
|
|
497
|
+
type: 'deleteMessage',
|
|
498
|
+
id: 'nonexistentMessage',
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const newState = messagesReducer(initialState, payload);
|
|
502
|
+
expect(newState).toEqual(initialState);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
306
505
|
});
|