@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/docs/Home.md +1 -1
  5. package/docs/_Sidebar.md +1 -1
  6. package/locales/de-DE/chat.json +10 -0
  7. package/locales/de-DE/common.json +1 -0
  8. package/locales/de-DE/error.json +4 -4
  9. package/locales/en-US/chat.json +10 -0
  10. package/locales/en-US/common.json +1 -0
  11. package/locales/en-US/error.json +1 -1
  12. package/locales/es-ES/chat.json +10 -0
  13. package/locales/es-ES/common.json +1 -0
  14. package/locales/fr-FR/chat.json +10 -0
  15. package/locales/fr-FR/common.json +1 -0
  16. package/locales/fr-FR/error.json +1 -2
  17. package/locales/it-IT/chat.json +10 -0
  18. package/locales/it-IT/common.json +1 -0
  19. package/locales/it-IT/error.json +5 -5
  20. package/locales/ja-JP/chat.json +10 -0
  21. package/locales/ja-JP/common.json +1 -0
  22. package/locales/ko-KR/chat.json +10 -0
  23. package/locales/ko-KR/common.json +1 -0
  24. package/locales/ko-KR/error.json +4 -4
  25. package/locales/nl-NL/chat.json +10 -0
  26. package/locales/nl-NL/common.json +1 -0
  27. package/locales/pl-PL/chat.json +10 -0
  28. package/locales/pl-PL/common.json +1 -0
  29. package/locales/pl-PL/error.json +2 -3
  30. package/locales/pt-BR/chat.json +10 -0
  31. package/locales/pt-BR/common.json +1 -0
  32. package/locales/ru-RU/chat.json +10 -0
  33. package/locales/ru-RU/common.json +1 -0
  34. package/locales/ru-RU/error.json +3 -3
  35. package/locales/tr-TR/chat.json +10 -0
  36. package/locales/tr-TR/common.json +1 -0
  37. package/locales/vi-VN/chat.json +10 -0
  38. package/locales/vi-VN/common.json +1 -0
  39. package/locales/vi-VN/error.json +1 -1
  40. package/locales/zh-CN/chat.json +10 -0
  41. package/locales/zh-CN/common.json +1 -0
  42. package/locales/zh-TW/chat.json +10 -0
  43. package/locales/zh-TW/common.json +1 -0
  44. package/package.json +6 -6
  45. package/src/app/chat/(desktop)/features/Conversation.tsx +17 -0
  46. package/src/app/chat/(desktop)/features/HotKeys.tsx +44 -0
  47. package/src/app/chat/(desktop)/index.tsx +2 -3
  48. package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
  49. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +24 -3
  50. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +43 -2
  51. package/src/database/models/__tests__/message.test.ts +48 -0
  52. package/src/database/models/__tests__/topic.test.ts +135 -0
  53. package/src/database/models/message.ts +35 -0
  54. package/src/database/models/session.ts +11 -1
  55. package/src/database/models/topic.ts +34 -2
  56. package/src/features/ChatInput/ActionBar/Clear.tsx +0 -6
  57. package/src/features/Conversation/Actions/Assistant.tsx +16 -5
  58. package/src/features/Conversation/Actions/Error.tsx +2 -2
  59. package/src/features/Conversation/Actions/Fallback.tsx +4 -3
  60. package/src/features/Conversation/Actions/Function.tsx +3 -3
  61. package/src/features/Conversation/Actions/User.tsx +3 -3
  62. package/src/features/Conversation/Actions/customAction.ts +2 -4
  63. package/src/features/Conversation/Actions/index.ts +50 -45
  64. package/src/features/Conversation/Error/InvalidAccess.tsx +1 -1
  65. package/src/features/Conversation/Error/OpenAPIKey.tsx +1 -1
  66. package/src/features/Conversation/Error/OpenAiBizError.tsx +1 -1
  67. package/src/features/Conversation/Error/Plugin/PluginError.tsx +1 -1
  68. package/src/features/Conversation/Error/Plugin/PluginSettings.tsx +1 -1
  69. package/src/features/Conversation/Error/index.tsx +2 -2
  70. package/src/features/Conversation/Extras/Assistant.tsx +32 -29
  71. package/src/features/Conversation/Extras/User.tsx +2 -1
  72. package/src/features/Conversation/Extras/index.ts +4 -4
  73. package/src/features/Conversation/Messages/index.ts +3 -3
  74. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +58 -0
  75. package/src/features/Conversation/components/ChatItem/index.tsx +126 -0
  76. package/src/features/Conversation/components/ChatList/HistoryDivider.tsx +6 -3
  77. package/src/features/Conversation/components/ChatList/index.tsx +35 -79
  78. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +43 -31
  79. package/src/features/Conversation/hooks/useInitConversation.ts +4 -1
  80. package/src/features/Conversation/index.tsx +4 -3
  81. package/src/features/Conversation/types/index.tsx +35 -0
  82. package/src/features/PluginStore/PluginItem/EditCustomPlugin.tsx +7 -2
  83. package/src/locales/default/chat.ts +10 -1
  84. package/src/locales/default/common.ts +1 -0
  85. package/src/services/config.ts +22 -0
  86. package/src/services/session.ts +6 -0
  87. package/src/services/topic.ts +4 -0
  88. package/src/store/chat/slices/message/action.ts +89 -2
  89. package/src/store/chat/slices/message/selectors.ts +10 -0
  90. package/src/store/chat/slices/topic/action.ts +22 -0
  91. package/src/store/global/slices/common/action.ts +2 -0
  92. package/src/store/session/slices/session/action.test.ts +136 -0
  93. package/src/store/session/slices/session/action.ts +26 -6
  94. package/src/types/topic.ts +1 -0
  95. package/src/utils/fetch.test.ts +131 -0
  96. package/src/utils/fetch.ts +26 -10
  97. package/src/features/Conversation/components/ChatList/ActionsBar.tsx +0 -27
  98. package/src/features/Conversation/components/ChatList/Item.tsx +0 -236
  99. package/src/features/Conversation/container/index.tsx +0 -83
  100. /package/docs/Development/{Steup-Development.md → Setup-Development.md} +0 -0
  101. /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) => ({ ...i, favorite: !!i.favorite }));
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(({ text, id, onActionClick, error }) => {
10
- const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);
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} text={text} />;
15
+ if (error) return <ErrorActionsBar onActionClick={onActionClick} />;
15
16
 
16
17
  return (
17
18
  <ActionIconGroup
18
- dropdownMenu={[edit, copy, regenerate, divider, tts, translate, divider, del]}
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>(({ text, onActionClick }) => {
8
- const { regenerate, del } = useChatListActionsBar(text);
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(({ text, onActionClick }) => {
8
- const { regenerate, divider, del } = useChatListActionsBar(text);
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(({ text, onActionClick }) => {
9
- const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);
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 { ChatListProps } from '../components/ChatList';
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: ChatListProps['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
- interface ActionsClick {
17
- onClick: () => void;
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
- return (action, { id, error }) => {
30
- const actionsClick: ActionsClick[] = [
31
- {
32
- onClick: () => {
33
- deleteMessage(id);
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
- actionsClick.find((item) => item.trigger)?.onClick();
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 '../components/ChatList';
11
+ import { RenderErrorMessage } from '../types';
12
12
  import APIKeyForm from './ApiKeyForm';
13
13
  import { ErrorActionContainer, FormAction } from './style';
14
14
 
@@ -1,6 +1,6 @@
1
1
  import { memo } from 'react';
2
2
 
3
- import { RenderErrorMessage } from '../components/ChatList';
3
+ import { RenderErrorMessage } from '../types';
4
4
  import APIKeyForm from './ApiKeyForm';
5
5
  import { ErrorActionContainer } from './style';
6
6
 
@@ -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 '../components/ChatList';
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 '../../components/ChatList';
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 '../../components/ChatList';
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 { ChatListProps } from '../components/ChatList';
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: ChatListProps['renderErrorMessages'] = {
12
+ export const renderErrorMessages: Record<string, RenderErrorMessage> = {
13
13
  [PluginErrorType.PluginMarketIndexNotFound]: {
14
14
  Render: PluginError,
15
15
  },