@lobehub/chat 0.118.10 → 0.119.1
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 +61 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/Home.md +1 -1
- package/docs/_Sidebar.md +1 -1
- package/locales/de-DE/chat.json +10 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/error.json +4 -4
- package/locales/en-US/chat.json +10 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/error.json +1 -1
- package/locales/es-ES/chat.json +10 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/fr-FR/chat.json +10 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/error.json +1 -2
- package/locales/it-IT/chat.json +10 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/error.json +5 -5
- package/locales/ja-JP/chat.json +10 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ko-KR/chat.json +10 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/error.json +4 -4
- package/locales/nl-NL/chat.json +10 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/pl-PL/chat.json +10 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/error.json +2 -3
- package/locales/pt-BR/chat.json +10 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/ru-RU/chat.json +10 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/error.json +3 -3
- package/locales/tr-TR/chat.json +10 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/vi-VN/chat.json +10 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/error.json +1 -1
- package/locales/zh-CN/chat.json +10 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-TW/chat.json +10 -0
- package/locales/zh-TW/common.json +1 -0
- package/package.json +6 -6
- package/src/app/chat/(desktop)/features/Conversation.tsx +17 -0
- package/src/app/chat/(desktop)/features/HotKeys.tsx +44 -0
- package/src/app/chat/(desktop)/index.tsx +2 -3
- package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
- package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +24 -3
- package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +43 -2
- package/src/database/models/__tests__/message.test.ts +48 -0
- package/src/database/models/__tests__/topic.test.ts +135 -0
- package/src/database/models/message.ts +35 -0
- package/src/database/models/session.ts +11 -1
- package/src/database/models/topic.ts +34 -2
- package/src/features/ChatInput/ActionBar/Clear.tsx +0 -6
- package/src/features/Conversation/Actions/Assistant.tsx +16 -5
- package/src/features/Conversation/Actions/Error.tsx +2 -2
- package/src/features/Conversation/Actions/Fallback.tsx +4 -3
- package/src/features/Conversation/Actions/Function.tsx +3 -3
- package/src/features/Conversation/Actions/User.tsx +3 -3
- package/src/features/Conversation/Actions/customAction.ts +2 -4
- package/src/features/Conversation/Actions/index.ts +50 -45
- package/src/features/Conversation/Error/InvalidAccess.tsx +1 -1
- package/src/features/Conversation/Error/OpenAPIKey.tsx +1 -1
- package/src/features/Conversation/Error/OpenAiBizError.tsx +1 -1
- package/src/features/Conversation/Error/Plugin/PluginError.tsx +1 -1
- package/src/features/Conversation/Error/Plugin/PluginSettings.tsx +1 -1
- package/src/features/Conversation/Error/index.tsx +2 -2
- package/src/features/Conversation/Extras/Assistant.tsx +32 -29
- package/src/features/Conversation/Extras/User.tsx +2 -1
- package/src/features/Conversation/Extras/index.ts +4 -4
- package/src/features/Conversation/Messages/index.ts +3 -3
- package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +58 -0
- package/src/features/Conversation/components/ChatItem/index.tsx +126 -0
- package/src/features/Conversation/components/ChatList/HistoryDivider.tsx +6 -3
- package/src/features/Conversation/components/ChatList/index.tsx +35 -79
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +43 -31
- package/src/features/Conversation/hooks/useInitConversation.ts +4 -1
- package/src/features/Conversation/index.tsx +4 -3
- package/src/features/Conversation/types/index.tsx +35 -0
- package/src/features/PluginStore/PluginItem/EditCustomPlugin.tsx +7 -2
- package/src/locales/default/chat.ts +10 -1
- package/src/locales/default/common.ts +1 -0
- package/src/services/config.ts +22 -0
- package/src/services/session.ts +6 -0
- package/src/services/topic.ts +4 -0
- package/src/store/chat/slices/message/action.ts +89 -2
- package/src/store/chat/slices/message/selectors.ts +10 -0
- package/src/store/chat/slices/topic/action.ts +22 -0
- package/src/store/global/slices/common/action.ts +2 -0
- package/src/store/session/slices/session/action.test.ts +136 -0
- package/src/store/session/slices/session/action.ts +26 -6
- package/src/types/topic.ts +1 -0
- package/src/utils/fetch.test.ts +131 -0
- package/src/utils/fetch.ts +26 -10
- package/src/features/Conversation/components/ChatList/ActionsBar.tsx +0 -27
- package/src/features/Conversation/components/ChatList/Item.tsx +0 -236
- package/src/features/Conversation/container/index.tsx +0 -83
- /package/docs/Development/{Steup-Development.md → Setup-Development.md} +0 -0
- /package/docs/Development/{Steup-Development.zh-CN.md → Setup-Development.zh-CN.md} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { DB_Message } from '@/database/schemas/message';
|
|
3
4
|
import { ChatMessage } from '@/types/message';
|
|
4
5
|
|
|
5
6
|
import { CreateMessageParams, MessageModel } from '../message';
|
|
@@ -324,4 +325,51 @@ describe('MessageModel', () => {
|
|
|
324
325
|
expect(messagesInDb).toHaveLength(0);
|
|
325
326
|
});
|
|
326
327
|
});
|
|
328
|
+
|
|
329
|
+
describe('duplicateMessages', () => {
|
|
330
|
+
it('should duplicate messages and update parentId for copied messages', async () => {
|
|
331
|
+
// 创建原始消息和父消息
|
|
332
|
+
const parentMessageData: DB_Message = {
|
|
333
|
+
content: 'Parent message content',
|
|
334
|
+
role: 'user',
|
|
335
|
+
sessionId: 'session1',
|
|
336
|
+
};
|
|
337
|
+
const parentMessage = await MessageModel.create(parentMessageData);
|
|
338
|
+
|
|
339
|
+
const childMessageData: DB_Message = {
|
|
340
|
+
content: 'Child message content',
|
|
341
|
+
role: 'user',
|
|
342
|
+
sessionId: 'session1',
|
|
343
|
+
parentId: parentMessage.id,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await MessageModel.create(childMessageData);
|
|
347
|
+
|
|
348
|
+
// 获取数据库中的消息以进行复制
|
|
349
|
+
const originalMessages = await MessageModel.queryAll();
|
|
350
|
+
|
|
351
|
+
// 执行复制操作
|
|
352
|
+
const duplicatedMessages = await MessageModel.duplicateMessages(originalMessages);
|
|
353
|
+
|
|
354
|
+
// 验证复制的消息数量是否正确
|
|
355
|
+
expect(duplicatedMessages.length).toBe(originalMessages.length);
|
|
356
|
+
|
|
357
|
+
// 验证每个复制的消息是否具有新的唯一ID,并且parentId被正确更新
|
|
358
|
+
for (const original of originalMessages) {
|
|
359
|
+
const copied = duplicatedMessages.find((m) => m.content === original.content);
|
|
360
|
+
expect(copied).toBeDefined();
|
|
361
|
+
expect(copied).not.toBeNull();
|
|
362
|
+
expect(copied!.id).not.toBe(original.id);
|
|
363
|
+
if (original.parentId) {
|
|
364
|
+
const originalParent = originalMessages.find((m) => m.id === original.parentId);
|
|
365
|
+
expect(originalParent).toBeDefined();
|
|
366
|
+
const copiedParent = duplicatedMessages.find(
|
|
367
|
+
(m) => m.content === originalParent!.content,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(copied!.parentId).toBe(copiedParent!.id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
327
375
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { DBModel } from '@/database/core/types/db';
|
|
3
4
|
import { CreateMessageParams, MessageModel } from '@/database/models/message';
|
|
4
5
|
import { DB_Message } from '@/database/schemas/message';
|
|
6
|
+
import { DB_Topic } from '@/database/schemas/topic';
|
|
5
7
|
import { nanoid } from '@/utils/uuid';
|
|
6
8
|
import * as uuidUtils from '@/utils/uuid';
|
|
7
9
|
|
|
@@ -287,4 +289,137 @@ describe('TopicModel', () => {
|
|
|
287
289
|
expect(messagesInDb1).toHaveLength(0);
|
|
288
290
|
expect(messagesInDb2).toHaveLength(0);
|
|
289
291
|
});
|
|
292
|
+
|
|
293
|
+
describe('duplicateTopic', () => {
|
|
294
|
+
let originalTopic: DBModel<DB_Topic>;
|
|
295
|
+
let originalMessages: any[];
|
|
296
|
+
|
|
297
|
+
beforeEach(async () => {
|
|
298
|
+
// 创建一个原始主题
|
|
299
|
+
const { id } = await TopicModel.create({
|
|
300
|
+
title: 'Original Topic',
|
|
301
|
+
sessionId: 'session1',
|
|
302
|
+
favorite: false,
|
|
303
|
+
});
|
|
304
|
+
originalTopic = await TopicModel.findById(id);
|
|
305
|
+
|
|
306
|
+
// 创建一些关联到原始主题的消息
|
|
307
|
+
originalMessages = await Promise.all(
|
|
308
|
+
['Message 1', 'Message 2'].map((text) =>
|
|
309
|
+
MessageModel.create({
|
|
310
|
+
content: text,
|
|
311
|
+
topicId: originalTopic.id,
|
|
312
|
+
sessionId: originalTopic.sessionId!,
|
|
313
|
+
role: 'user',
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
afterEach(async () => {
|
|
320
|
+
// 清理数据库中的所有主题和消息
|
|
321
|
+
await TopicModel.clearTable();
|
|
322
|
+
await MessageModel.clearTable();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should duplicate a topic with all associated messages', async () => {
|
|
326
|
+
// 执行复制操作
|
|
327
|
+
await TopicModel.duplicateTopic(originalTopic.id);
|
|
328
|
+
|
|
329
|
+
// 验证复制后的主题是否存在
|
|
330
|
+
const duplicatedTopic = await TopicModel.findBySessionId(originalTopic.sessionId!);
|
|
331
|
+
expect(duplicatedTopic).toHaveLength(2);
|
|
332
|
+
|
|
333
|
+
// 验证复制后的消息是否存在
|
|
334
|
+
const duplicatedMessages = await MessageModel.query({
|
|
335
|
+
sessionId: originalTopic.sessionId!,
|
|
336
|
+
topicId: duplicatedTopic[1].id, // 假设复制的主题是第二个
|
|
337
|
+
});
|
|
338
|
+
expect(duplicatedMessages).toHaveLength(originalMessages.length);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should throw an error if the topic does not exist', async () => {
|
|
342
|
+
// 尝试复制一个不存在的主题
|
|
343
|
+
const nonExistentTopicId = nanoid();
|
|
344
|
+
await expect(TopicModel.duplicateTopic(nonExistentTopicId)).rejects.toThrow(
|
|
345
|
+
`Topic with id ${nonExistentTopicId} not found`,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should preserve the properties of the duplicated topic', async () => {
|
|
350
|
+
// 执行复制操作
|
|
351
|
+
await TopicModel.duplicateTopic(originalTopic.id);
|
|
352
|
+
|
|
353
|
+
// 获取复制的主题
|
|
354
|
+
const topics = await TopicModel.findBySessionId(originalTopic.sessionId!);
|
|
355
|
+
const duplicatedTopic = topics.find((topic) => topic.id !== originalTopic.id);
|
|
356
|
+
|
|
357
|
+
// 验证复制的主题是否保留了原始主题的属性
|
|
358
|
+
expect(duplicatedTopic).toBeDefined();
|
|
359
|
+
expect(duplicatedTopic).toMatchObject({
|
|
360
|
+
title: originalTopic.title,
|
|
361
|
+
favorite: originalTopic.favorite,
|
|
362
|
+
sessionId: originalTopic.sessionId,
|
|
363
|
+
});
|
|
364
|
+
// 确保生成了新的 ID
|
|
365
|
+
expect(duplicatedTopic.id).not.toBe(originalTopic.id);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should properly handle the messages hierarchy when duplicating', async () => {
|
|
369
|
+
// 创建一个子消息关联到其中一个原始消息
|
|
370
|
+
const { id } = await MessageModel.create({
|
|
371
|
+
content: 'Child Message',
|
|
372
|
+
topicId: originalTopic.id,
|
|
373
|
+
parentId: originalMessages[0].id,
|
|
374
|
+
sessionId: originalTopic.sessionId!,
|
|
375
|
+
role: 'user',
|
|
376
|
+
});
|
|
377
|
+
const childMessage = await MessageModel.findById(id);
|
|
378
|
+
|
|
379
|
+
// 执行复制操作
|
|
380
|
+
await TopicModel.duplicateTopic(originalTopic.id);
|
|
381
|
+
|
|
382
|
+
// 获取复制的消息
|
|
383
|
+
const duplicatedMessages = await MessageModel.queryBySessionId(originalTopic.sessionId!);
|
|
384
|
+
|
|
385
|
+
// 验证复制的子消息是否存在并且 parentId 已更新
|
|
386
|
+
const duplicatedChildMessage = duplicatedMessages.find(
|
|
387
|
+
(message) => message.content === childMessage.content && message.id !== childMessage.id,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect(duplicatedChildMessage).toBeDefined();
|
|
391
|
+
expect(duplicatedChildMessage.parentId).not.toBe(childMessage.parentId);
|
|
392
|
+
expect(duplicatedChildMessage.parentId).toBeDefined();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should fail if the database transaction fails', async () => {
|
|
396
|
+
// 强制数据库事务失败,例如通过在复制过程中抛出异常
|
|
397
|
+
const dbTransactionFailedError = new Error('DB transaction failed');
|
|
398
|
+
vi.spyOn(TopicModel['db'], 'transaction').mockImplementation((async () => {
|
|
399
|
+
throw dbTransactionFailedError;
|
|
400
|
+
}) as any);
|
|
401
|
+
|
|
402
|
+
// 尝试复制主题并捕捉期望的错误
|
|
403
|
+
await expect(TopicModel.duplicateTopic(originalTopic.id)).rejects.toThrow(
|
|
404
|
+
dbTransactionFailedError,
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should not create partial duplicates if the process fails at some point', async () => {
|
|
409
|
+
// 假设复制消息的过程中发生了错误
|
|
410
|
+
vi.spyOn(MessageModel, 'duplicateMessages').mockImplementation(async () => {
|
|
411
|
+
throw new Error('Failed to duplicate messages');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// 尝试复制主题,期望会抛出错误
|
|
415
|
+
await expect(TopicModel.duplicateTopic(originalTopic.id)).rejects.toThrow();
|
|
416
|
+
|
|
417
|
+
// 确保没有创建任何副本
|
|
418
|
+
const topics = await TopicModel.findBySessionId(originalTopic.sessionId!);
|
|
419
|
+
expect(topics).toHaveLength(1); // 只有原始主题
|
|
420
|
+
|
|
421
|
+
const messages = await MessageModel.queryBySessionId(originalTopic.sessionId!);
|
|
422
|
+
expect(messages).toHaveLength(originalMessages.length); // 只有原始消息
|
|
423
|
+
});
|
|
424
|
+
});
|
|
290
425
|
});
|
|
@@ -168,6 +168,41 @@ class _MessageModel extends BaseModel {
|
|
|
168
168
|
return this.table.where('sessionId').equals(sessionId).toArray();
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
queryByTopicId = async (topicId: string) => {
|
|
172
|
+
const dbMessages = await this.table.where('topicId').equals(topicId).toArray();
|
|
173
|
+
|
|
174
|
+
return dbMessages.map((message) => this.mapToChatMessage(message));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
async duplicateMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
|
|
178
|
+
const duplicatedMessages = await this.createDuplicateMessages(messages);
|
|
179
|
+
// 批量添加复制后的消息到数据库
|
|
180
|
+
await this.batchCreate(duplicatedMessages);
|
|
181
|
+
return duplicatedMessages;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async createDuplicateMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
|
|
185
|
+
// 创建一个映射来存储原始消息ID和复制消息ID之间的关系
|
|
186
|
+
const idMapping = new Map<string, string>();
|
|
187
|
+
|
|
188
|
+
// 首先复制所有消息,并为每个复制的消息生成新的ID
|
|
189
|
+
const duplicatedMessages = messages.map((originalMessage) => {
|
|
190
|
+
const newId = nanoid();
|
|
191
|
+
idMapping.set(originalMessage.id, newId);
|
|
192
|
+
|
|
193
|
+
return { ...originalMessage, id: newId };
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 更新 parentId 为复制后的新ID
|
|
197
|
+
for (const duplicatedMessage of duplicatedMessages) {
|
|
198
|
+
if (duplicatedMessage.parentId && idMapping.has(duplicatedMessage.parentId)) {
|
|
199
|
+
duplicatedMessage.parentId = idMapping.get(duplicatedMessage.parentId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return duplicatedMessages;
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
private mapChatMessageToDBMessage(message: ChatMessage): DB_Message {
|
|
172
207
|
const { extra, ...messageData } = message;
|
|
173
208
|
|
|
@@ -2,6 +2,7 @@ import { DeepPartial } from 'utility-types';
|
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
|
|
4
4
|
import { BaseModel } from '@/database/core';
|
|
5
|
+
import { DBModel } from '@/database/core/types/db';
|
|
5
6
|
import { DB_Session, DB_SessionSchema } from '@/database/schemas/session';
|
|
6
7
|
import { LobeAgentConfig } from '@/types/agent';
|
|
7
8
|
import { LobeAgentSession, LobeSessions, SessionGroupKey } from '@/types/session';
|
|
@@ -79,7 +80,7 @@ class _SessionModel extends BaseModel {
|
|
|
79
80
|
return this.table.clear();
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
async findById(id: string) {
|
|
83
|
+
async findById(id: string): Promise<DBModel<DB_Session>> {
|
|
83
84
|
return this.table.get(id);
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -155,6 +156,15 @@ class _SessionModel extends BaseModel {
|
|
|
155
156
|
console.timeEnd('queryByKeyword');
|
|
156
157
|
return data;
|
|
157
158
|
}
|
|
159
|
+
|
|
160
|
+
async duplicate(id: string, newTitle?: string) {
|
|
161
|
+
const session = await this.findById(id);
|
|
162
|
+
if (!session) return;
|
|
163
|
+
|
|
164
|
+
const newSession = merge(session, { meta: { title: newTitle } });
|
|
165
|
+
|
|
166
|
+
return this._add(newSession, uuid());
|
|
167
|
+
}
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
export const SessionModel = new _SessionModel();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BaseModel } from '@/database/core';
|
|
2
|
+
import { DBModel } from '@/database/core/types/db';
|
|
3
|
+
import { MessageModel } from '@/database/models/message';
|
|
2
4
|
import { DB_Topic, DB_TopicSchema } from '@/database/schemas/topic';
|
|
3
5
|
import { ChatTopic } from '@/types/topic';
|
|
4
6
|
import { nanoid } from '@/utils/uuid';
|
|
@@ -53,14 +55,14 @@ class _TopicModel extends BaseModel {
|
|
|
53
55
|
// handle pageSize
|
|
54
56
|
const pagedTopics = sortedTopics.slice(offset, offset + pageSize);
|
|
55
57
|
|
|
56
|
-
return pagedTopics.map((i) => (
|
|
58
|
+
return pagedTopics.map((i) => this.mapToChatTopic(i));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
async findBySessionId(sessionId: string) {
|
|
60
62
|
return this.table.where({ sessionId }).toArray();
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
async findById(id: string) {
|
|
65
|
+
async findById(id: string): Promise<DBModel<DB_Topic>> {
|
|
64
66
|
return this.table.get(id);
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -198,6 +200,36 @@ class _TopicModel extends BaseModel {
|
|
|
198
200
|
console.timeEnd('queryTopicsByKeyword');
|
|
199
201
|
return uniqueTopics.map((i) => ({ ...i, favorite: !!i.favorite }));
|
|
200
202
|
}
|
|
203
|
+
|
|
204
|
+
async duplicateTopic(topicId: string, newTitle?: string) {
|
|
205
|
+
return this.db.transaction('rw', this.db.topics, this.db.messages, async () => {
|
|
206
|
+
// Step 1: get DB_Topic
|
|
207
|
+
const topic = await this.findById(topicId);
|
|
208
|
+
|
|
209
|
+
if (!topic) {
|
|
210
|
+
throw new Error(`Topic with id ${topicId} not found`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Step 3: 查询与 `topic` 关联的 `messages`
|
|
214
|
+
const originalMessages = await MessageModel.queryByTopicId(topicId);
|
|
215
|
+
|
|
216
|
+
const duplicateMessages = await MessageModel.duplicateMessages(originalMessages);
|
|
217
|
+
|
|
218
|
+
const { id } = await this.create({
|
|
219
|
+
...this.mapToChatTopic(topic),
|
|
220
|
+
messages: duplicateMessages.map((m) => m.id),
|
|
221
|
+
sessionId: topic.sessionId!,
|
|
222
|
+
title: newTitle || topic.title,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return id;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private mapToChatTopic = (dbTopic: DBModel<DB_Topic>): ChatTopic => ({
|
|
230
|
+
...dbTopic,
|
|
231
|
+
favorite: !!dbTopic.favorite,
|
|
232
|
+
});
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
export const TopicModel = new _TopicModel();
|
|
@@ -2,7 +2,6 @@ import { ActionIcon } from '@lobehub/ui';
|
|
|
2
2
|
import { Popconfirm } from 'antd';
|
|
3
3
|
import { Eraser } from 'lucide-react';
|
|
4
4
|
import { memo, useCallback } from 'react';
|
|
5
|
-
import { useHotkeys } from 'react-hotkeys-hook';
|
|
6
5
|
import { useTranslation } from 'react-i18next';
|
|
7
6
|
|
|
8
7
|
import HotKeys from '@/components/HotKeys';
|
|
@@ -21,11 +20,6 @@ const Clear = memo(() => {
|
|
|
21
20
|
clearImageList();
|
|
22
21
|
}, []);
|
|
23
22
|
|
|
24
|
-
useHotkeys(hotkeys, resetConversation, {
|
|
25
|
-
enableOnFormTags: true,
|
|
26
|
-
preventDefault: true,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
23
|
return (
|
|
30
24
|
<Popconfirm
|
|
31
25
|
cancelText={t('cancel', { ns: 'common' })}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { ActionIconGroup } from '@lobehub/ui';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
|
|
4
|
-
import { RenderAction } from '../components/ChatList';
|
|
5
4
|
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
5
|
+
import { RenderAction } from '../types';
|
|
6
6
|
import { ErrorActionsBar } from './Error';
|
|
7
7
|
import { useCustomActions } from './customAction';
|
|
8
8
|
|
|
9
|
-
export const AssistantActionsBar: RenderAction = memo(({
|
|
10
|
-
const { regenerate, edit, copy, divider, del } = useChatListActionsBar(
|
|
9
|
+
export const AssistantActionsBar: RenderAction = memo(({ id, onActionClick, error }) => {
|
|
10
|
+
const { regenerate, edit, delAndRegenerate, copy, divider, del } = useChatListActionsBar();
|
|
11
11
|
const { translate, tts } = useCustomActions();
|
|
12
|
+
|
|
12
13
|
if (id === 'default') return;
|
|
13
14
|
|
|
14
|
-
if (error) return <ErrorActionsBar onActionClick={onActionClick}
|
|
15
|
+
if (error) return <ErrorActionsBar onActionClick={onActionClick} />;
|
|
15
16
|
|
|
16
17
|
return (
|
|
17
18
|
<ActionIconGroup
|
|
18
|
-
dropdownMenu={[
|
|
19
|
+
dropdownMenu={[
|
|
20
|
+
edit,
|
|
21
|
+
copy,
|
|
22
|
+
regenerate,
|
|
23
|
+
divider,
|
|
24
|
+
tts,
|
|
25
|
+
translate,
|
|
26
|
+
divider,
|
|
27
|
+
delAndRegenerate,
|
|
28
|
+
del,
|
|
29
|
+
]}
|
|
19
30
|
items={[regenerate, copy]}
|
|
20
31
|
onActionClick={onActionClick}
|
|
21
32
|
type="ghost"
|
|
@@ -4,8 +4,8 @@ import { memo } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
6
6
|
|
|
7
|
-
export const ErrorActionsBar = memo<ActionsBarProps>(({
|
|
8
|
-
const { regenerate, del } = useChatListActionsBar(
|
|
7
|
+
export const ErrorActionsBar = memo<ActionsBarProps>(({ onActionClick }) => {
|
|
8
|
+
const { regenerate, del } = useChatListActionsBar();
|
|
9
9
|
|
|
10
10
|
return <ActionIconGroup items={[regenerate, del]} onActionClick={onActionClick} type="ghost" />;
|
|
11
11
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ActionIconGroup } from '@lobehub/ui';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
|
|
4
|
-
import { RenderAction } from '../components/ChatList';
|
|
5
4
|
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
5
|
+
import { RenderAction } from '../types';
|
|
6
|
+
|
|
7
|
+
export const DefaultActionsBar: RenderAction = memo(({ onActionClick }) => {
|
|
8
|
+
const { del } = useChatListActionsBar();
|
|
6
9
|
|
|
7
|
-
export const DefaultActionsBar: RenderAction = memo(({ text, onActionClick }) => {
|
|
8
|
-
const { del } = useChatListActionsBar(text);
|
|
9
10
|
return (
|
|
10
11
|
<ActionIconGroup dropdownMenu={[del]} items={[]} onActionClick={onActionClick} type="ghost" />
|
|
11
12
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ActionIconGroup } from '@lobehub/ui';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
|
|
4
|
-
import { RenderAction } from '../components/ChatList';
|
|
5
4
|
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
5
|
+
import { RenderAction } from '../types';
|
|
6
6
|
|
|
7
|
-
export const FunctionActionsBar: RenderAction = memo(({
|
|
8
|
-
const { regenerate, divider, del } = useChatListActionsBar(
|
|
7
|
+
export const FunctionActionsBar: RenderAction = memo(({ onActionClick }) => {
|
|
8
|
+
const { regenerate, divider, del } = useChatListActionsBar();
|
|
9
9
|
return (
|
|
10
10
|
<ActionIconGroup
|
|
11
11
|
dropdownMenu={[regenerate, divider, del]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ActionIconGroup } from '@lobehub/ui';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
|
|
4
|
-
import { RenderAction } from '../components/ChatList';
|
|
5
4
|
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
5
|
+
import { RenderAction } from '../types';
|
|
6
6
|
import { useCustomActions } from './customAction';
|
|
7
7
|
|
|
8
|
-
export const UserActionsBar: RenderAction = memo(({
|
|
9
|
-
const { regenerate, edit, copy, divider, del } = useChatListActionsBar(
|
|
8
|
+
export const UserActionsBar: RenderAction = memo(({ onActionClick }) => {
|
|
9
|
+
const { regenerate, edit, copy, divider, del } = useChatListActionsBar();
|
|
10
10
|
const { translate, tts } = useCustomActions();
|
|
11
11
|
|
|
12
12
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ActionIconGroupItems } from '@lobehub/ui/es/ActionIconGroup';
|
|
2
2
|
import { LanguagesIcon, Play } from 'lucide-react';
|
|
3
|
+
import { useMemo } from 'react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
|
|
5
6
|
import { localeOptions } from '@/locales/resources';
|
|
@@ -23,8 +24,5 @@ export const useCustomActions = () => {
|
|
|
23
24
|
label: t('tts.action'),
|
|
24
25
|
} as ActionIconGroupItems;
|
|
25
26
|
|
|
26
|
-
return {
|
|
27
|
-
translate,
|
|
28
|
-
tts,
|
|
29
|
-
};
|
|
27
|
+
return useMemo(() => ({ translate, tts }), []);
|
|
30
28
|
};
|
|
@@ -1,67 +1,72 @@
|
|
|
1
|
+
import { copyToClipboard } from '@lobehub/ui';
|
|
2
|
+
import { App } from 'antd';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
1
6
|
import { useChatStore } from '@/store/chat';
|
|
7
|
+
import { LLMRoleType } from '@/types/llm';
|
|
2
8
|
|
|
3
|
-
import {
|
|
9
|
+
import { OnActionsClick, RenderAction } from '../types';
|
|
4
10
|
import { AssistantActionsBar } from './Assistant';
|
|
5
11
|
import { DefaultActionsBar } from './Fallback';
|
|
6
12
|
import { FunctionActionsBar } from './Function';
|
|
7
13
|
import { UserActionsBar } from './User';
|
|
8
14
|
|
|
9
|
-
export const renderActions:
|
|
15
|
+
export const renderActions: Record<LLMRoleType, RenderAction> = {
|
|
10
16
|
assistant: AssistantActionsBar,
|
|
11
17
|
function: FunctionActionsBar,
|
|
12
18
|
system: DefaultActionsBar,
|
|
13
19
|
user: UserActionsBar,
|
|
14
20
|
};
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
trigger: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const useActionsClick = (): ChatListProps['onActionsClick'] => {
|
|
22
|
+
export const useActionsClick = (): OnActionsClick => {
|
|
23
|
+
const { t } = useTranslation('common');
|
|
22
24
|
const [deleteMessage, resendMessage, translateMessage, ttsMessage] = useChatStore((s) => [
|
|
23
25
|
s.deleteMessage,
|
|
24
26
|
s.resendMessage,
|
|
25
27
|
s.translateMessage,
|
|
26
28
|
s.ttsMessage,
|
|
27
29
|
]);
|
|
30
|
+
const { message } = App.useApp();
|
|
31
|
+
|
|
32
|
+
return useCallback<OnActionsClick>(async (action, { id, content, error }) => {
|
|
33
|
+
switch (action.key) {
|
|
34
|
+
case 'copy': {
|
|
35
|
+
await copyToClipboard(content);
|
|
36
|
+
message.success(t('copySuccess', { defaultValue: 'Copy Success' }));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case 'del': {
|
|
41
|
+
deleteMessage(id);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'regenerate': {
|
|
46
|
+
resendMessage(id);
|
|
47
|
+
// if this message is an error message, we need to delete it
|
|
48
|
+
if (error) deleteMessage(id);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'delAndRegenerate': {
|
|
53
|
+
resendMessage(id);
|
|
54
|
+
deleteMessage(id);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
trigger: action.key === 'del',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
onClick: () => {
|
|
39
|
-
resendMessage(id);
|
|
40
|
-
// if this message is an error message, we need to delete it
|
|
41
|
-
if (error) deleteMessage(id);
|
|
42
|
-
},
|
|
43
|
-
trigger: action.key === 'regenerate',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
onClick: () => {
|
|
47
|
-
ttsMessage(id);
|
|
48
|
-
},
|
|
49
|
-
trigger: action.key === 'tts',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
onClick: () => {
|
|
53
|
-
/**
|
|
54
|
-
* @description Click the menu item with translate item, the result is:
|
|
55
|
-
* @key 'en-US'
|
|
56
|
-
* @keyPath ['en-US','translate']
|
|
57
|
-
*/
|
|
58
|
-
const lang = action.keyPath[0];
|
|
59
|
-
translateMessage(id, lang);
|
|
60
|
-
},
|
|
61
|
-
trigger: action.keyPath.at(-1) === 'translate',
|
|
62
|
-
},
|
|
63
|
-
];
|
|
58
|
+
case 'tts': {
|
|
59
|
+
ttsMessage(id);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
if (action.keyPath.at(-1) === 'translate') {
|
|
65
|
+
// click the menu item with translate item, the result is:
|
|
66
|
+
// key: 'en-US'
|
|
67
|
+
// keyPath: ['en-US','translate']
|
|
68
|
+
const lang = action.keyPath[0];
|
|
69
|
+
translateMessage(id, lang);
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
67
72
|
};
|
|
@@ -8,7 +8,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
8
8
|
import { useChatStore } from '@/store/chat';
|
|
9
9
|
import { useGlobalStore } from '@/store/global';
|
|
10
10
|
|
|
11
|
-
import { RenderErrorMessage } from '../
|
|
11
|
+
import { RenderErrorMessage } from '../types';
|
|
12
12
|
import APIKeyForm from './ApiKeyForm';
|
|
13
13
|
import { ErrorActionContainer, FormAction } from './style';
|
|
14
14
|
|
|
@@ -2,7 +2,7 @@ import { Highlighter } from '@lobehub/ui';
|
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { Flexbox } from 'react-layout-kit';
|
|
4
4
|
|
|
5
|
-
import { RenderErrorMessage } from '../
|
|
5
|
+
import { RenderErrorMessage } from '../types';
|
|
6
6
|
import OpenAPIKey from './OpenAPIKey';
|
|
7
7
|
|
|
8
8
|
interface OpenAIError {
|
|
@@ -2,7 +2,7 @@ import { Highlighter } from '@lobehub/ui';
|
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { Flexbox } from 'react-layout-kit';
|
|
4
4
|
|
|
5
|
-
import { RenderErrorMessage } from '../../
|
|
5
|
+
import { RenderErrorMessage } from '../../types';
|
|
6
6
|
|
|
7
7
|
interface OpenAIError {
|
|
8
8
|
code: 'invalid_api_key' | string;
|
|
@@ -11,7 +11,7 @@ import { useChatStore } from '@/store/chat';
|
|
|
11
11
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
12
12
|
import { pluginSelectors } from '@/store/tool/selectors';
|
|
13
13
|
|
|
14
|
-
import { RenderErrorMessage } from '../../
|
|
14
|
+
import { RenderErrorMessage } from '../../types';
|
|
15
15
|
import { ErrorActionContainer, useStyles } from '../style';
|
|
16
16
|
|
|
17
17
|
const PluginSettings: RenderErrorMessage['Render'] = memo(({ id, plugin }) => {
|
|
@@ -2,14 +2,14 @@ import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
|
2
2
|
|
|
3
3
|
import { ChatErrorType } from '@/types/fetch';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { RenderErrorMessage } from '../types';
|
|
6
6
|
import InvalidAccess from './InvalidAccess';
|
|
7
7
|
import OpenAPIKey from './OpenAPIKey';
|
|
8
8
|
import OpenAiBizError from './OpenAiBizError';
|
|
9
9
|
import PluginError from './Plugin/PluginError';
|
|
10
10
|
import PluginSettings from './Plugin/PluginSettings';
|
|
11
11
|
|
|
12
|
-
export const renderErrorMessages:
|
|
12
|
+
export const renderErrorMessages: Record<string, RenderErrorMessage> = {
|
|
13
13
|
[PluginErrorType.PluginMarketIndexNotFound]: {
|
|
14
14
|
Render: PluginError,
|
|
15
15
|
},
|