@lobehub/chat 0.118.10 → 0.119.0

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 (92) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/locales/de-DE/chat.json +10 -0
  3. package/locales/de-DE/common.json +1 -0
  4. package/locales/de-DE/error.json +4 -4
  5. package/locales/en-US/chat.json +10 -0
  6. package/locales/en-US/common.json +1 -0
  7. package/locales/en-US/error.json +1 -1
  8. package/locales/es-ES/chat.json +10 -0
  9. package/locales/es-ES/common.json +1 -0
  10. package/locales/fr-FR/chat.json +10 -0
  11. package/locales/fr-FR/common.json +1 -0
  12. package/locales/fr-FR/error.json +1 -2
  13. package/locales/it-IT/chat.json +10 -0
  14. package/locales/it-IT/common.json +1 -0
  15. package/locales/it-IT/error.json +5 -5
  16. package/locales/ja-JP/chat.json +10 -0
  17. package/locales/ja-JP/common.json +1 -0
  18. package/locales/ko-KR/chat.json +10 -0
  19. package/locales/ko-KR/common.json +1 -0
  20. package/locales/ko-KR/error.json +4 -4
  21. package/locales/nl-NL/chat.json +10 -0
  22. package/locales/nl-NL/common.json +1 -0
  23. package/locales/pl-PL/chat.json +10 -0
  24. package/locales/pl-PL/common.json +1 -0
  25. package/locales/pl-PL/error.json +2 -3
  26. package/locales/pt-BR/chat.json +10 -0
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/ru-RU/chat.json +10 -0
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/error.json +3 -3
  31. package/locales/tr-TR/chat.json +10 -0
  32. package/locales/tr-TR/common.json +1 -0
  33. package/locales/vi-VN/chat.json +10 -0
  34. package/locales/vi-VN/common.json +1 -0
  35. package/locales/vi-VN/error.json +1 -1
  36. package/locales/zh-CN/chat.json +10 -0
  37. package/locales/zh-CN/common.json +1 -0
  38. package/locales/zh-TW/chat.json +10 -0
  39. package/locales/zh-TW/common.json +1 -0
  40. package/package.json +2 -2
  41. package/src/app/chat/(desktop)/features/Conversation.tsx +17 -0
  42. package/src/app/chat/(desktop)/features/HotKeys.tsx +44 -0
  43. package/src/app/chat/(desktop)/index.tsx +2 -3
  44. package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
  45. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +24 -3
  46. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +43 -2
  47. package/src/database/models/__tests__/message.test.ts +48 -0
  48. package/src/database/models/__tests__/topic.test.ts +135 -0
  49. package/src/database/models/message.ts +35 -0
  50. package/src/database/models/session.ts +11 -1
  51. package/src/database/models/topic.ts +34 -2
  52. package/src/features/ChatInput/ActionBar/Clear.tsx +0 -6
  53. package/src/features/Conversation/Actions/Assistant.tsx +16 -5
  54. package/src/features/Conversation/Actions/Error.tsx +2 -2
  55. package/src/features/Conversation/Actions/Fallback.tsx +4 -3
  56. package/src/features/Conversation/Actions/Function.tsx +3 -3
  57. package/src/features/Conversation/Actions/User.tsx +3 -3
  58. package/src/features/Conversation/Actions/customAction.ts +2 -4
  59. package/src/features/Conversation/Actions/index.ts +50 -45
  60. package/src/features/Conversation/Error/InvalidAccess.tsx +1 -1
  61. package/src/features/Conversation/Error/OpenAPIKey.tsx +1 -1
  62. package/src/features/Conversation/Error/OpenAiBizError.tsx +1 -1
  63. package/src/features/Conversation/Error/Plugin/PluginError.tsx +1 -1
  64. package/src/features/Conversation/Error/Plugin/PluginSettings.tsx +1 -1
  65. package/src/features/Conversation/Error/index.tsx +2 -2
  66. package/src/features/Conversation/Extras/Assistant.tsx +32 -29
  67. package/src/features/Conversation/Extras/User.tsx +2 -1
  68. package/src/features/Conversation/Extras/index.ts +4 -4
  69. package/src/features/Conversation/Messages/index.ts +3 -3
  70. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +58 -0
  71. package/src/features/Conversation/components/ChatItem/index.tsx +126 -0
  72. package/src/features/Conversation/components/ChatList/HistoryDivider.tsx +6 -3
  73. package/src/features/Conversation/components/ChatList/index.tsx +35 -79
  74. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +43 -31
  75. package/src/features/Conversation/hooks/useInitConversation.ts +4 -1
  76. package/src/features/Conversation/index.tsx +4 -3
  77. package/src/features/Conversation/types/index.tsx +35 -0
  78. package/src/features/PluginStore/PluginItem/EditCustomPlugin.tsx +7 -2
  79. package/src/locales/default/chat.ts +10 -1
  80. package/src/locales/default/common.ts +1 -0
  81. package/src/services/config.ts +22 -0
  82. package/src/services/session.ts +6 -0
  83. package/src/services/topic.ts +4 -0
  84. package/src/store/chat/slices/message/selectors.ts +10 -0
  85. package/src/store/chat/slices/topic/action.ts +22 -0
  86. package/src/store/global/slices/common/action.ts +2 -0
  87. package/src/store/session/slices/session/action.test.ts +136 -0
  88. package/src/store/session/slices/session/action.ts +26 -6
  89. package/src/types/topic.ts +1 -0
  90. package/src/features/Conversation/components/ChatList/ActionsBar.tsx +0 -27
  91. package/src/features/Conversation/components/ChatList/Item.tsx +0 -236
  92. package/src/features/Conversation/container/index.tsx +0 -83
@@ -9,9 +9,6 @@
9
9
  "403": "Извините, сервер отклонил ваш запрос. У вас нет разрешения на доступ к этому содержимому",
10
10
  "404": "Извините, сервер не может найти запрошенную вами страницу или ресурс. Убедитесь, что ваш URL-адрес указан правильно.",
11
11
  "405": "К сожалению, сервер не поддерживает используемый вами метод запроса. Убедитесь, что ваш метод запроса правильный.",
12
- "429": "Извините, ваш запрос слишком частый, и сервер немного устал. Повторите попытку позже.",
13
- "500": "К сожалению, сервер, похоже, испытывает некоторые трудности и временно не может выполнить ваш запрос. Повторите попытку позже.",
14
- "502": "К сожалению, сервер, похоже, потерян и временно не может предоставлять услуги. Повторите попытку позже.",
15
12
  "406": "Извините, сервер не может выполнить запрос в соответствии с характеристиками запрошенного контента",
16
13
  "407": "Извините, для продолжения запроса необходима аутентификация прокси",
17
14
  "408": "Извините, сервер превысил время ожидания при ожидании запроса. Пожалуйста, проверьте свое сетевое подключение и повторите попытку",
@@ -29,8 +26,11 @@
29
26
  "424": "Извините, из-за предыдущей неудачной попытки запрос не может быть выполнен",
30
27
  "426": "Извините, сервер требует обновления вашего клиента до более новой версии протокола",
31
28
  "428": "Извините, сервер требует предварительных условий, ваш запрос должен содержать правильные условные заголовки",
29
+ "429": "Извините, ваш запрос слишком частый, и сервер немного устал. Повторите попытку позже.",
32
30
  "431": "Извините, ваш заголовок запроса слишком велик, сервер не может его обработать",
33
31
  "451": "Извините, по юридическим причинам сервер отказывается предоставить этот ресурс",
32
+ "500": "К сожалению, сервер, похоже, испытывает некоторые трудности и временно не может выполнить ваш запрос. Повторите попытку позже.",
33
+ "502": "К сожалению, сервер, похоже, потерян и временно не может предоставлять услуги. Повторите попытку позже.",
34
34
  "503": "К сожалению, сервер в настоящее время не может обработать ваш запрос, возможно, из-за перегрузки или технического обслуживания. Повторите попытку позже.",
35
35
  "504": "К сожалению, сервер не получил ответа от вышестоящего сервера. Повторите попытку позже.",
36
36
  "InvalidAccessCode": "Пароль неверен или пуст. Введите правильный пароль доступа или добавьте собственный ключ API OpenAI.",
@@ -7,12 +7,17 @@
7
7
  "confirmRemoveSessionItemAlert": "Bu asistanı silmek üzeresiniz. Silindikten sonra geri alınamaz. Lütfen eyleminizi onaylayın.",
8
8
  "defaultAgent": "Varsayılan Asistan",
9
9
  "defaultSession": "Varsayılan Asistan",
10
+ "duplicateTitle": "{{title}} Kopya",
10
11
  "historyRange": "Geçmiş Aralığı",
11
12
  "inbox": {
12
13
  "defaultMessage": "Merhaba, ben sanal asistanınızım. Bana herhangi bir şey sorabilirsiniz ve size cevap vermek için elimden geleni yapacağım. Daha profesyonel veya özelleştirilmiş bir asistana ihtiyacınız varsa, özel bir asistan oluşturmak için `+` simgesine tıklayabilirsiniz.",
13
14
  "desc": "Beyin fırtınasını başlatın ve yaratıcı düşünmeye başlayın. Sanal asistanınız burada, her konuda sizinle iletişim kurmak için hazır.",
14
15
  "title": "Sohbet Et"
15
16
  },
17
+ "messageAction": {
18
+ "delAndRegenerate": "Sil ve Yeniden Oluştur",
19
+ "regenerate": "Yeniden Oluştur"
20
+ },
16
21
  "newAgent": "Yeni Asistan",
17
22
  "noDescription": "Açıklama yok",
18
23
  "pin": "Pin",
@@ -48,6 +53,11 @@
48
53
  "used": "Kullanılan"
49
54
  },
50
55
  "topic": {
56
+ "actions": {
57
+ "autoRename": "Akıllı Yeniden Adlandırma",
58
+ "duplicate": "Kopya Oluştur",
59
+ "export": "Konuyu Dışa Aktar"
60
+ },
51
61
  "confirmRemoveAll": "Tüm konuları silmek üzeresiniz. Bir kere silindiğinde, geri alınamazlar. Lütfen dikkatli bir şekilde devam edin.",
52
62
  "confirmRemoveTopic": "Bu konuyu silmek üzeresiniz. Bir kere silindiğinde, geri alınamaz. Lütfen dikkatli bir şekilde devam edin.",
53
63
  "confirmRemoveUnstarred": "Yıldızlanmamış konuları silmek üzeresiniz. Bir kere silindiğinde, geri alınamazlar. Lütfen dikkatli bir şekilde devam edin.",
@@ -17,6 +17,7 @@
17
17
  "defaultAgent": "Varsayılan Asistan",
18
18
  "defaultSession": "Varsayılan Asistan",
19
19
  "delete": "Sil",
20
+ "duplicate": "Kopya oluştur",
20
21
  "edit": "Düzenle",
21
22
  "export": "Dışa Aktar",
22
23
  "exportType": {
@@ -7,12 +7,17 @@
7
7
  "confirmRemoveSessionItemAlert": "Bạn sắp xóa trợ lý này. Hành động này không thể hoàn tác, vui lòng xác nhận.",
8
8
  "defaultAgent": "Trợ lý mặc định",
9
9
  "defaultSession": "Trợ lý mặc định",
10
+ "duplicateTitle": "{{title}} Bản sao",
10
11
  "historyRange": "Phạm vi lịch sử",
11
12
  "inbox": {
12
13
  "defaultMessage": "Xin chào, tôi là trợ lý thông minh của bạn, bạn có thể hỏi tôi bất kỳ câu hỏi nào và tôi sẽ cố gắng trả lời. Nếu bạn cần một trợ lý chuyên nghiệp hoặc tùy chỉnh hơn, hãy nhấn `+` để tạo trợ lý tùy chỉnh.",
13
14
  "desc": "Kích hoạt cụm não, khơi dậy tia lửa tư duy. Trợ lý thông minh của bạn, ở đây để trò chuyện với bạn về mọi thứ.",
14
15
  "title": "Chuyện phiếm"
15
16
  },
17
+ "messageAction": {
18
+ "delAndRegenerate": "Xóa và tạo lại",
19
+ "regenerate": "Tạo lại"
20
+ },
16
21
  "newAgent": "Tạo trợ lý mới",
17
22
  "noDescription": "Chưa có mô tả",
18
23
  "pin": "Ghim",
@@ -48,6 +53,11 @@
48
53
  "used": "Đã sử dụng"
49
54
  },
50
55
  "topic": {
56
+ "actions": {
57
+ "autoRename": "Đổi tên tự động",
58
+ "duplicate": "Tạo bản sao",
59
+ "export": "Xuất chủ đề"
60
+ },
51
61
  "confirmRemoveAll": "Bạn sắp xóa tất cả chủ đề. Hành động này không thể hoàn tác, vui lòng xác nhận.",
52
62
  "confirmRemoveTopic": "Bạn sắp xóa chủ đề này. Hành động này không thể hoàn tác, vui lòng xác nhận.",
53
63
  "confirmRemoveUnstarred": "Bạn sắp xóa các chủ đề chưa được đánh dấu. Hành động này không thể hoàn tác, vui lòng xác nhận.",
@@ -17,6 +17,7 @@
17
17
  "defaultAgent": "Trợ lý mặc định",
18
18
  "defaultSession": "Phiên mặc định",
19
19
  "delete": "Xóa",
20
+ "duplicate": "Tạo bản sao",
20
21
  "edit": "Chỉnh sửa",
21
22
  "export": "Xuất cấu hình",
22
23
  "exportType": {
@@ -9,7 +9,6 @@
9
9
  "403": "Xin lỗi, máy chủ từ chối yêu cầu của bạn, bạn không có quyền truy cập nội dung này",
10
10
  "404": "Xin lỗi, máy chủ không tìm thấy trang hoặc tài nguyên bạn yêu cầu, vui lòng xác nhận URL của bạn có đúng không",
11
11
  "405": "Xin lỗi, máy chủ không hỗ trợ phương thức yêu cầu bạn đang sử dụng, vui lòng xác nhận phương thức yêu cầu của bạn có đúng không",
12
- "429": "Xin lỗi, yêu cầu của bạn quá nhiều, máy chủ hơi mệt, vui lòng thử lại sau",
13
12
  "406": "Xin lỗi, máy chủ không thể hoàn thành yêu cầu dựa trên đặc tính nội dung bạn yêu cầu",
14
13
  "407": "Xin lỗi, bạn cần xác thực proxy trước khi tiếp tục yêu cầu này",
15
14
  "408": "Xin lỗi, máy chủ đã vượt quá thời gian chờ khi đang chờ đợi yêu cầu, vui lòng kiểm tra kết nối mạng của bạn và thử lại",
@@ -27,6 +26,7 @@
27
26
  "424": "Xin lỗi, yêu cầu hiện tại không thể hoàn thành do yêu cầu trước đó thất bại",
28
27
  "426": "Xin lỗi, máy chủ yêu cầu bạn nâng cấp phiên bản giao thức của khách hàng lên cao hơn",
29
28
  "428": "Xin lỗi, máy chủ yêu cầu điều kiện tiên quyết, yêu cầu của bạn phải chứa tiêu đề điều kiện chính xác",
29
+ "429": "Xin lỗi, yêu cầu của bạn quá nhiều, máy chủ hơi mệt, vui lòng thử lại sau",
30
30
  "431": "Xin lỗi, trường tiêu đề yêu cầu của bạn quá lớn, máy chủ không thể xử lý",
31
31
  "451": "Xin lỗi, do lý do pháp lý, máy chủ từ chối cung cấp tài nguyên này",
32
32
  "500": "Xin lỗi, máy chủ có vẻ gặp một số khó khăn, tạm thời không thể hoàn thành yêu cầu của bạn, vui lòng thử lại sau",
@@ -7,12 +7,17 @@
7
7
  "confirmRemoveSessionItemAlert": "即将删除该助手,删除后该将无法找回,请确认你的操作",
8
8
  "defaultAgent": "自定义助手",
9
9
  "defaultSession": "自定义助手",
10
+ "duplicateTitle": "{{title}} 副本",
10
11
  "historyRange": "历史范围",
11
12
  "inbox": {
12
13
  "defaultMessage": "你好,我是你的智能助手,你可以问我任何问题,我会尽力回答你。如果需要获得更加专业或定制的助手,可以点击`+`创建自定义助手",
13
14
  "desc": "开启大脑集群,激发思维火花。你的智能助理,在这里与你交流一切",
14
15
  "title": "随便聊聊"
15
16
  },
17
+ "messageAction": {
18
+ "delAndRegenerate": "删除并重新生成",
19
+ "regenerate": "重新生成"
20
+ },
16
21
  "newAgent": "新建助手",
17
22
  "noDescription": "暂无描述",
18
23
  "pin": "置顶",
@@ -48,6 +53,11 @@
48
53
  "used": "使用"
49
54
  },
50
55
  "topic": {
56
+ "actions": {
57
+ "autoRename": "智能重命名",
58
+ "duplicate": "创建副本",
59
+ "export": "导出话题"
60
+ },
51
61
  "confirmRemoveAll": "即将删除全部话题,删除后将不可恢复,请谨慎操作。",
52
62
  "confirmRemoveTopic": "即将删除该话题,删除后将不可恢复,请谨慎操作。",
53
63
  "confirmRemoveUnstarred": "即将删除未收藏话题,删除后将不可恢复,请谨慎操作。",
@@ -17,6 +17,7 @@
17
17
  "defaultAgent": "自定义助手",
18
18
  "defaultSession": "自定义助手",
19
19
  "delete": "删除",
20
+ "duplicate": "创建副本",
20
21
  "edit": "编辑",
21
22
  "export": "导出配置",
22
23
  "exportType": {
@@ -7,12 +7,17 @@
7
7
  "confirmRemoveSessionItemAlert": "即將刪除該助手,刪除後將無法找回,請確認你的操作",
8
8
  "defaultAgent": "自定義助手",
9
9
  "defaultSession": "自定義助手",
10
+ "duplicateTitle": "{{title}} 副本",
10
11
  "historyRange": "歷史範圍",
11
12
  "inbox": {
12
13
  "defaultMessage": "你好,我是你的智能助手,你可以問我任何問題,我會盡力回答你。如果需要獲得更專業或定制的助手,可以點擊`+`創建自定義助手",
13
14
  "desc": "開啟大腦集群,激發思維火花。你的智能助理,在這裡與你交流一切",
14
15
  "title": "隨便聊聊"
15
16
  },
17
+ "messageAction": {
18
+ "delAndRegenerate": "刪除並重新生成",
19
+ "regenerate": "重新生成"
20
+ },
16
21
  "newAgent": "新建助手",
17
22
  "noDescription": "暫無描述",
18
23
  "pin": "置頂",
@@ -48,6 +53,11 @@
48
53
  "used": "使用"
49
54
  },
50
55
  "topic": {
56
+ "actions": {
57
+ "autoRename": "智能重新命名",
58
+ "duplicate": "建立副本",
59
+ "export": "匯出主題"
60
+ },
51
61
  "confirmRemoveAll": "即將刪除全部話題,刪除後將不可恢復,請謹慎操作。",
52
62
  "confirmRemoveTopic": "即將刪除該話題,刪除後將不可恢復,請謹慎操作。",
53
63
  "confirmRemoveUnstarred": "即將刪除未收藏話題,刪除後將不可恢復,請謹慎操作。",
@@ -17,6 +17,7 @@
17
17
  "defaultAgent": "預設助手",
18
18
  "defaultSession": "預設助手",
19
19
  "delete": "刪除",
20
+ "duplicate": "建立副本",
20
21
  "edit": "編輯",
21
22
  "export": "匯出設定",
22
23
  "exportType": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.118.10",
3
+ "version": "0.119.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -107,7 +107,7 @@
107
107
  "query-string": "^8",
108
108
  "react": "^18",
109
109
  "react-dom": "^18",
110
- "react-hotkeys-hook": "^4",
110
+ "react-hotkeys-hook": "4.4.1",
111
111
  "react-i18next": "^14.0.0",
112
112
  "react-intersection-observer": "^9",
113
113
  "react-layout-kit": "^1",
@@ -0,0 +1,17 @@
1
+ import { memo } from 'react';
2
+
3
+ import RawConversation from '@/features/Conversation';
4
+
5
+ import ChatInput from './ChatInput';
6
+ import HotKeys from './HotKeys';
7
+
8
+ const Conversation = memo(() => {
9
+ return (
10
+ <>
11
+ <RawConversation chatInput={<ChatInput />} />
12
+ <HotKeys />
13
+ </>
14
+ );
15
+ });
16
+
17
+ export default Conversation;
@@ -0,0 +1,44 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { useCallback } from 'react';
3
+ import { useHotkeys } from 'react-hotkeys-hook';
4
+
5
+ import { CLEAN_MESSAGE_KEY, META_KEY, PREFIX_KEY, REGENERATE_KEY } from '@/const/hotkeys';
6
+ import { useChatStore } from '@/store/chat';
7
+ import { chatSelectors } from '@/store/chat/selectors';
8
+ import { useFileStore } from '@/store/file';
9
+
10
+ const HotKeys = () => {
11
+ const resendHotkeys = [PREFIX_KEY, REGENERATE_KEY].join('+');
12
+
13
+ const [resendMessage, clearMessage] = useChatStore((s) => [s.resendMessage, s.clearMessage]);
14
+ const lastMessage = useChatStore(chatSelectors.latestMessage, isEqual);
15
+
16
+ const [clearImageList] = useFileStore((s) => [s.clearImageList]);
17
+ const clearHotkeys = [META_KEY, PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
18
+
19
+ const resetConversation = useCallback(() => {
20
+ clearMessage();
21
+ clearImageList();
22
+ }, []);
23
+
24
+ useHotkeys(clearHotkeys, resetConversation, {
25
+ enableOnFormTags: true,
26
+ preventDefault: true,
27
+ });
28
+
29
+ useHotkeys(
30
+ resendHotkeys,
31
+ () => {
32
+ if (!lastMessage || lastMessage.id === 'default' || lastMessage.role === 'system') return;
33
+ resendMessage(lastMessage.id);
34
+ },
35
+ {
36
+ enableOnFormTags: true,
37
+ preventDefault: true,
38
+ },
39
+ );
40
+
41
+ return null;
42
+ };
43
+
44
+ export default HotKeys;
@@ -5,11 +5,10 @@ import { FC, memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import ResponsiveIndex from '@/components/ResponsiveIndex';
8
- import Conversation from '@/features/Conversation';
9
8
 
10
9
  import PageTitle from '../features/PageTitle';
11
10
  import ChatHeader from './features/ChatHeader';
12
- import ChatInput from './features/ChatInput';
11
+ import Conversation from './features/Conversation';
13
12
  import SideBar from './features/SideBar';
14
13
  import Layout from './layout.desktop';
15
14
 
@@ -21,7 +20,7 @@ const DesktopPage = memo(() => (
21
20
  <PageTitle />
22
21
  <ChatHeader />
23
22
  <Flexbox flex={1} height={'calc(100% - 64px)'} horizontal>
24
- <Conversation chatInput={<ChatInput />} />
23
+ <Conversation />
25
24
  <SideBar />
26
25
  </Flexbox>
27
26
  </Layout>
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import pkg from '@/../package.json';
8
- import ChatList from '@/features/Conversation/container';
8
+ import ChatList from '@/features/Conversation/components/ChatList';
9
9
  import { useSessionStore } from '@/store/session';
10
10
  import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
11
11
 
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon, Icon } from '@lobehub/ui';
2
2
  import { App, Dropdown, type MenuProps } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
- import { HardDriveDownload, MoreVertical, Pin, PinOff, Trash } from 'lucide-react';
4
+ import { HardDriveDownload, LucideCopy, MoreVertical, Pin, PinOff, Trash } from 'lucide-react';
5
5
  import { memo, useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
@@ -26,9 +26,14 @@ const Actions = memo<ActionProps>(({ id, setOpen }) => {
26
26
 
27
27
  const { styles } = useStyles();
28
28
 
29
- const [pin, removeSession, pinSession] = useSessionStore((s) => {
29
+ const [pin, removeSession, pinSession, duplicateSession] = useSessionStore((s) => {
30
30
  const session = sessionSelectors.getSessionById(id)(s);
31
- return [sessionHelpers.getSessionPinned(session), s.removeSession, s.pinSession];
31
+ return [
32
+ sessionHelpers.getSessionPinned(session),
33
+ s.removeSession,
34
+ s.pinSession,
35
+ s.duplicateSession,
36
+ ];
32
37
  });
33
38
 
34
39
  const { modal } = App.useApp();
@@ -64,6 +69,22 @@ const Actions = memo<ActionProps>(({ id, setOpen }) => {
64
69
  key: 'export',
65
70
  label: t('export'),
66
71
  },
72
+ {
73
+ type: 'divider',
74
+ },
75
+ {
76
+ icon: <Icon icon={LucideCopy} />,
77
+ key: 'duplicate',
78
+ label: t('duplicate'),
79
+ onClick: ({ domEvent }) => {
80
+ domEvent.stopPropagation();
81
+
82
+ duplicateSession(id);
83
+ },
84
+ },
85
+ {
86
+ type: 'divider',
87
+ },
67
88
  {
68
89
  danger: true,
69
90
  icon: <Icon icon={Trash} />,
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon, EditableText, Icon } from '@lobehub/ui';
2
2
  import { App, Dropdown, type MenuProps, Typography } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
- import { MoreVertical, PencilLine, Star, Trash } from 'lucide-react';
4
+ import { LucideCopy, MoreVertical, PencilLine, Star, Trash, Wand2 } from 'lucide-react';
5
5
  import { memo, useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
@@ -33,11 +33,22 @@ interface TopicContentProps {
33
33
  const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
34
34
  const { t } = useTranslation('common');
35
35
 
36
- const [editing, favoriteTopic, updateTopicTitle, removeTopic] = useChatStore((s) => [
36
+ const [
37
+ editing,
38
+ favoriteTopic,
39
+ updateTopicTitle,
40
+ removeTopic,
41
+ autoRenameTopicTitle,
42
+ duplicateTopic,
43
+ // sessionId,
44
+ ] = useChatStore((s) => [
37
45
  s.topicRenamingId === id,
38
46
  s.favoriteTopic,
39
47
  s.updateTopicTitle,
40
48
  s.removeTopic,
49
+ s.autoRenameTopicTitle,
50
+ s.duplicateTopic,
51
+ s.activeId,
41
52
  ]);
42
53
  const { styles, theme } = useStyles();
43
54
 
@@ -49,6 +60,14 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
49
60
 
50
61
  const items = useMemo<MenuProps['items']>(
51
62
  () => [
63
+ {
64
+ icon: <Icon icon={Wand2} />,
65
+ key: 'autoRename',
66
+ label: t('topic.actions.autoRename', { ns: 'chat' }),
67
+ onClick: () => {
68
+ autoRenameTopicTitle(id);
69
+ },
70
+ },
52
71
  {
53
72
  icon: <Icon icon={PencilLine} />,
54
73
  key: 'rename',
@@ -57,6 +76,28 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
57
76
  toggleEditing(true);
58
77
  },
59
78
  },
79
+ {
80
+ type: 'divider',
81
+ },
82
+ {
83
+ icon: <Icon icon={LucideCopy} />,
84
+ key: 'duplicate',
85
+ label: t('topic.actions.duplicate', { ns: 'chat' }),
86
+ onClick: () => {
87
+ duplicateTopic(id);
88
+ },
89
+ },
90
+ // {
91
+ // icon: <Icon icon={LucideDownload} />,
92
+ // key: 'export',
93
+ // label: t('topic.actions.export', { ns: 'chat' }),
94
+ // onClick: () => {
95
+ // configService.exportSingleTopic(sessionId, id);
96
+ // },
97
+ // },
98
+ {
99
+ type: 'divider',
100
+ },
60
101
  // {
61
102
  // icon: <Icon icon={Share2} />,
62
103
  // key: 'share',
@@ -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
+ });
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
  });