@lobehub/lobehub 2.0.0-next.187 → 2.0.0-next.189

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 (177) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/e2e/CLAUDE.md +109 -2
  4. package/e2e/docs/llm-mock.md +68 -0
  5. package/e2e/docs/local-setup.md +354 -0
  6. package/e2e/docs/testing-tips.md +94 -0
  7. package/e2e/src/features/journeys/agent/agent-conversation.feature +0 -32
  8. package/e2e/src/mocks/llm/index.ts +6 -6
  9. package/e2e/src/steps/agent/conversation.steps.ts +3 -471
  10. package/locales/ar/models.json +89 -5
  11. package/locales/ar/plugin.json +5 -0
  12. package/locales/ar/providers.json +1 -0
  13. package/locales/bg-BG/models.json +68 -0
  14. package/locales/bg-BG/plugin.json +5 -0
  15. package/locales/bg-BG/providers.json +1 -0
  16. package/locales/de-DE/models.json +85 -0
  17. package/locales/de-DE/plugin.json +5 -0
  18. package/locales/de-DE/providers.json +1 -0
  19. package/locales/en-US/models.json +11 -10
  20. package/locales/en-US/plugin.json +5 -0
  21. package/locales/en-US/providers.json +1 -0
  22. package/locales/es-ES/models.json +72 -0
  23. package/locales/es-ES/plugin.json +5 -0
  24. package/locales/es-ES/providers.json +1 -0
  25. package/locales/fa-IR/models.json +86 -0
  26. package/locales/fa-IR/plugin.json +5 -0
  27. package/locales/fa-IR/providers.json +1 -0
  28. package/locales/fr-FR/models.json +49 -0
  29. package/locales/fr-FR/plugin.json +5 -0
  30. package/locales/fr-FR/providers.json +1 -0
  31. package/locales/it-IT/models.json +82 -0
  32. package/locales/it-IT/plugin.json +5 -0
  33. package/locales/it-IT/providers.json +1 -0
  34. package/locales/ja-JP/models.json +42 -5
  35. package/locales/ja-JP/plugin.json +5 -0
  36. package/locales/ja-JP/providers.json +1 -0
  37. package/locales/ko-KR/models.json +54 -0
  38. package/locales/ko-KR/plugin.json +5 -0
  39. package/locales/ko-KR/providers.json +1 -0
  40. package/locales/nl-NL/models.json +12 -1
  41. package/locales/nl-NL/plugin.json +5 -0
  42. package/locales/nl-NL/providers.json +1 -0
  43. package/locales/pl-PL/models.json +46 -0
  44. package/locales/pl-PL/plugin.json +5 -0
  45. package/locales/pl-PL/providers.json +1 -0
  46. package/locales/pt-BR/models.json +59 -0
  47. package/locales/pt-BR/plugin.json +5 -0
  48. package/locales/pt-BR/providers.json +1 -0
  49. package/locales/ru-RU/models.json +85 -0
  50. package/locales/ru-RU/plugin.json +5 -0
  51. package/locales/ru-RU/providers.json +1 -0
  52. package/locales/tr-TR/models.json +81 -0
  53. package/locales/tr-TR/plugin.json +5 -0
  54. package/locales/tr-TR/providers.json +1 -0
  55. package/locales/vi-VN/models.json +54 -0
  56. package/locales/vi-VN/plugin.json +5 -0
  57. package/locales/vi-VN/providers.json +1 -0
  58. package/locales/zh-CN/models.json +42 -5
  59. package/locales/zh-CN/plugin.json +5 -0
  60. package/locales/zh-CN/providers.json +1 -0
  61. package/locales/zh-TW/models.json +85 -0
  62. package/locales/zh-TW/plugin.json +5 -0
  63. package/locales/zh-TW/providers.json +1 -0
  64. package/package.json +2 -2
  65. package/packages/builtin-tool-gtd/src/manifest.ts +13 -8
  66. package/packages/builtin-tool-gtd/src/systemRole.ts +54 -19
  67. package/packages/builtin-tool-knowledge-base/package.json +1 -0
  68. package/packages/builtin-tool-knowledge-base/src/client/Inspector/ReadKnowledge/index.tsx +97 -0
  69. package/packages/builtin-tool-knowledge-base/src/client/Inspector/SearchKnowledgeBase/index.tsx +75 -0
  70. package/packages/builtin-tool-knowledge-base/src/client/Inspector/index.ts +11 -0
  71. package/packages/builtin-tool-knowledge-base/src/client/Render/ReadKnowledge/FileCard.tsx +12 -12
  72. package/packages/builtin-tool-knowledge-base/src/client/Render/ReadKnowledge/index.tsx +16 -25
  73. package/packages/builtin-tool-knowledge-base/src/client/Render/SearchKnowledgeBase/Item/index.tsx +21 -47
  74. package/packages/builtin-tool-knowledge-base/src/client/Render/SearchKnowledgeBase/index.tsx +19 -31
  75. package/packages/builtin-tool-knowledge-base/src/client/Render/index.ts +0 -5
  76. package/packages/builtin-tool-knowledge-base/src/client/index.ts +5 -1
  77. package/packages/builtin-tool-knowledge-base/src/executor/index.ts +119 -0
  78. package/packages/builtin-tool-local-system/package.json +1 -0
  79. package/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx +44 -29
  80. package/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx +20 -18
  81. package/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx +76 -0
  82. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +8 -32
  83. package/packages/builtin-tool-local-system/src/client/Inspector/RenameLocalFile/index.tsx +62 -0
  84. package/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx +17 -11
  85. package/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx +61 -0
  86. package/packages/builtin-tool-local-system/src/client/Inspector/index.ts +6 -0
  87. package/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +6 -1
  88. package/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/SearchView.tsx +19 -31
  89. package/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx +2 -42
  90. package/packages/builtin-tool-local-system/src/client/Render/index.ts +0 -2
  91. package/packages/builtin-tool-local-system/src/client/components/FilePathDisplay.tsx +56 -0
  92. package/packages/builtin-tool-local-system/src/client/components/index.ts +2 -0
  93. package/packages/builtin-tool-local-system/src/executor/index.ts +435 -0
  94. package/packages/builtin-tool-web-browsing/src/client/Inspector/Search/index.tsx +32 -5
  95. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +84 -0
  96. package/packages/model-runtime/src/core/contextBuilders/google.ts +37 -1
  97. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
  98. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
  99. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
  100. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
  101. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
  102. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
  103. package/src/app/[variants]/(main)/community/(detail)/assistant/features/Sidebar/ActionButton/AddAgent.tsx +47 -27
  104. package/src/app/[variants]/(main)/community/(detail)/user/features/UserAgentCard.tsx +4 -3
  105. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
  106. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
  107. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
  108. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
  109. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
  110. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
  111. package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +18 -20
  112. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +19 -25
  113. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +21 -26
  114. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/Actions.tsx +4 -13
  115. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +3 -3
  116. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Actions.tsx +4 -13
  117. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +8 -15
  118. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +3 -3
  119. package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +3 -4
  120. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Actions.tsx +4 -13
  121. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +13 -20
  122. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +3 -3
  123. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/Actions.tsx +4 -13
  124. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/index.tsx +16 -23
  125. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/useDropdownMenu.tsx +3 -3
  126. package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +4 -6
  127. package/src/features/AgentBuilder/TopicSelector.tsx +18 -17
  128. package/src/features/Conversation/ChatItem/style.ts +7 -0
  129. package/src/features/Conversation/Messages/Assistant/Actions/Error.tsx +1 -3
  130. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +37 -16
  131. package/src/features/Conversation/Messages/AssistantGroup/Actions/index.tsx +36 -17
  132. package/src/features/Conversation/Messages/Supervisor/Actions/index.tsx +36 -17
  133. package/src/features/Conversation/Messages/Task/Actions/Error.tsx +1 -3
  134. package/src/features/Conversation/Messages/Task/Actions/index.tsx +31 -15
  135. package/src/features/Conversation/Messages/User/Actions/index.tsx +1 -1
  136. package/src/features/Conversation/Messages/index.tsx +8 -59
  137. package/src/features/Conversation/components/ShareMessageModal/index.tsx +1 -1
  138. package/src/features/Conversation/hooks/useChatItemContextMenu.tsx +313 -83
  139. package/src/features/NavPanel/components/NavItem.tsx +33 -3
  140. package/src/features/PageEditor/Copilot/TopicSelector/Actions.tsx +6 -14
  141. package/src/features/PageEditor/Copilot/TopicSelector/TopicItem.tsx +1 -0
  142. package/src/features/PageEditor/Copilot/TopicSelector/useDropdownMenu.tsx +6 -3
  143. package/src/features/ResourceManager/components/Explorer/ItemDropdown/DropdownMenu.tsx +12 -35
  144. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +4 -8
  145. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +162 -160
  146. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +16 -8
  147. package/src/features/ResourceManager/components/Explorer/ToolBar/ActionIconWithChevron.tsx +4 -3
  148. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +6 -12
  149. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +8 -8
  150. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +8 -11
  151. package/src/features/ResourceManager/components/Tree/index.tsx +121 -122
  152. package/src/helpers/toolEngineering/index.ts +1 -1
  153. package/src/layout/GlobalProvider/index.tsx +5 -2
  154. package/src/locales/default/plugin.ts +6 -0
  155. package/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts +1 -1
  156. package/src/server/modules/Mecha/AgentToolsEngine/index.ts +1 -1
  157. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -11
  158. package/src/store/tool/slices/builtin/executors/index.ts +4 -0
  159. package/src/styles/global.ts +6 -0
  160. package/src/styles/text.ts +1 -1
  161. package/src/tools/executionRuntimes.ts +3 -8
  162. package/src/tools/identifiers.ts +1 -1
  163. package/src/tools/index.ts +1 -1
  164. package/src/tools/inspectors.ts +5 -0
  165. package/src/tools/renders.ts +6 -12
  166. package/packages/builtin-tool-local-system/src/client/Render/RenameLocalFile/index.tsx +0 -37
  167. package/src/features/Conversation/components/ContextMenu.tsx +0 -418
  168. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +0 -201
  169. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +0 -163
  170. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +0 -241
  171. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +0 -25
  172. package/src/tools/knowledge-base/Render/ReadKnowledge/index.tsx +0 -29
  173. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/index.tsx +0 -29
  174. package/src/tools/knowledge-base/Render/index.ts +0 -7
  175. package/src/tools/knowledge-base/index.ts +0 -12
  176. package/src/tools/local-system/ExecutionRuntime/index.ts +0 -9
  177. package/src/tools/local-system/systemRole.ts +0 -1
@@ -1,49 +1,338 @@
1
- import { type ActionIconGroupEvent } from '@lobehub/ui';
2
- import type React from 'react';
3
- import { useCallback, useEffect, useRef, useState } from 'react';
1
+ import {
2
+ type ActionIconGroupEvent,
3
+ type ActionIconGroupItemType,
4
+ type DropdownItem,
5
+ type GenericItemType,
6
+ createRawModal,
7
+ showContextMenu,
8
+ } from '@lobehub/ui';
9
+ import { App } from 'antd';
10
+ import isEqual from 'fast-deep-equal';
11
+ import type { MouseEvent, ReactNode } from 'react';
12
+ import { useCallback, useMemo, useRef } from 'react';
13
+ import { useTranslation } from 'react-i18next';
4
14
 
5
15
  import { MSG_CONTENT_CLASSNAME } from '@/features/Conversation/ChatItem/components/MessageContent';
16
+ import { useSessionStore } from '@/store/session';
17
+ import { sessionSelectors } from '@/store/session/selectors';
6
18
  import { useUserStore } from '@/store/user';
7
19
  import { userGeneralSettingsSelectors } from '@/store/user/selectors';
8
20
 
9
- interface ContextMenuState {
10
- position: { x: number; y: number };
11
- selectedText?: string;
12
- visible: boolean;
21
+ import ShareMessageModal from '../components/ShareMessageModal';
22
+ import {
23
+ dataSelectors,
24
+ messageStateSelectors,
25
+ useConversationStore,
26
+ useConversationStoreApi,
27
+ } from '../store';
28
+ import { useChatListActionsBar } from './useChatListActionsBar';
29
+
30
+ interface ActionMenuItem extends ActionIconGroupItemType {
31
+ children?: { key: string; label: ReactNode }[];
32
+ disable?: boolean;
33
+ popupClassName?: string;
13
34
  }
14
35
 
36
+ type MenuItem = ActionMenuItem | { type: 'divider' };
37
+ type ContextMenuEvent = ActionIconGroupEvent & { selectedText?: string };
38
+
15
39
  interface UseChatItemContextMenuProps {
16
40
  editing?: boolean;
17
41
  id: string;
18
- onActionClick: (action: ActionIconGroupEvent) => void;
42
+ inPortalThread: boolean;
43
+ topic?: string | null;
19
44
  }
20
45
 
21
46
  export const useChatItemContextMenu = ({
22
- onActionClick,
23
47
  editing,
24
- }: Omit<UseChatItemContextMenuProps, 'id'>) => {
48
+ id,
49
+ inPortalThread,
50
+ topic,
51
+ }: UseChatItemContextMenuProps) => {
25
52
  const contextMenuMode = useUserStore(userGeneralSettingsSelectors.contextMenuMode);
53
+ const { message } = App.useApp();
54
+ const { t } = useTranslation('common');
55
+
56
+ const selectedTextRef = useRef<string | undefined>(undefined);
57
+
58
+ const storeApi = useConversationStoreApi();
59
+
60
+ const [role, error, isCollapsed, hasThread, isRegenerating] = useConversationStore((s) => {
61
+ const item = dataSelectors.getDisplayMessageById(id)(s);
62
+ return [
63
+ item?.role,
64
+ item?.error,
65
+ messageStateSelectors.isMessageCollapsed(id)(s),
66
+ messageStateSelectors.hasThreadBySourceMsgId(id)(s),
67
+ messageStateSelectors.isMessageRegenerating(id)(s),
68
+ ];
69
+ }, isEqual);
70
+
71
+ const isThreadMode = useConversationStore(messageStateSelectors.isThreadMode);
72
+ const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
73
+ const actionsBar = useChatListActionsBar({ hasThread, isRegenerating });
74
+ const inThread = isThreadMode || inPortalThread;
75
+
76
+ const [
77
+ toggleMessageEditing,
78
+ deleteMessage,
79
+ regenerateUserMessage,
80
+ regenerateAssistantMessage,
81
+ translateMessage,
82
+ ttsMessage,
83
+ delAndRegenerateMessage,
84
+ copyMessage,
85
+ openThreadCreator,
86
+ resendThreadMessage,
87
+ delAndResendThreadMessage,
88
+ toggleMessageCollapsed,
89
+ ] = useConversationStore((s) => [
90
+ s.toggleMessageEditing,
91
+ s.deleteMessage,
92
+ s.regenerateUserMessage,
93
+ s.regenerateAssistantMessage,
94
+ s.translateMessage,
95
+ s.ttsMessage,
96
+ s.delAndRegenerateMessage,
97
+ s.copyMessage,
98
+ s.openThreadCreator,
99
+ s.resendThreadMessage,
100
+ s.delAndResendThreadMessage,
101
+ s.toggleMessageCollapsed,
102
+ ]);
103
+
104
+ const getMessage = useCallback(
105
+ () => dataSelectors.getDisplayMessageById(id)(storeApi.getState()),
106
+ [id, storeApi],
107
+ );
108
+
109
+ const menuItems = useMemo<MenuItem[]>(() => {
110
+ if (!role) return [];
111
+
112
+ const {
113
+ branching,
114
+ collapse,
115
+ copy,
116
+ del,
117
+ delAndRegenerate,
118
+ divider,
119
+ edit,
120
+ expand,
121
+ regenerate,
122
+ share,
123
+ translate,
124
+ tts,
125
+ } = actionsBar;
126
+
127
+ if (role === 'assistant') {
128
+ if (error) {
129
+ return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
130
+ }
131
+
132
+ const collapseAction = isCollapsed ? expand : collapse;
133
+ const list: MenuItem[] = [edit, copy, collapseAction];
134
+
135
+ if (!inThread && !isGroupSession) list.push(branching);
136
+
137
+ list.push(
138
+ divider,
139
+ tts,
140
+ translate,
141
+ divider,
142
+ share,
143
+ divider,
144
+ regenerate,
145
+ delAndRegenerate,
146
+ del,
147
+ );
148
+
149
+ return list.filter(Boolean) as MenuItem[];
150
+ }
151
+
152
+ if (role === 'assistantGroup') {
153
+ if (error) {
154
+ return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
155
+ }
156
+
157
+ const collapseAction = isCollapsed ? expand : collapse;
158
+ const list: MenuItem[] = [
159
+ edit,
160
+ copy,
161
+ collapseAction,
162
+ divider,
163
+ share,
164
+ divider,
165
+ regenerate,
166
+ del,
167
+ ];
168
+
169
+ return list.filter(Boolean) as MenuItem[];
170
+ }
171
+
172
+ if (role === 'user') {
173
+ const list: MenuItem[] = [edit, copy];
174
+
175
+ if (!inThread) list.push(branching);
176
+
177
+ list.push(divider, tts, translate, divider, regenerate, del);
178
+
179
+ return list.filter(Boolean) as MenuItem[];
180
+ }
181
+
182
+ return [];
183
+ }, [actionsBar, error, inThread, isCollapsed, isGroupSession, role]);
26
184
 
27
- const [contextMenuState, setContextMenuState] = useState<ContextMenuState>({
28
- position: { x: 0, y: 0 },
29
- visible: false,
30
- });
185
+ const handleShare = useCallback(() => {
186
+ const item = getMessage();
187
+ if (!item || item.role !== 'assistant') return;
31
188
 
32
- const containerRef = useRef<HTMLDivElement>(null);
189
+ createRawModal(
190
+ ShareMessageModal,
191
+ {
192
+ message: item,
193
+ },
194
+ { onCloseKey: 'onCancel', openKey: 'open' },
195
+ );
196
+ }, [getMessage]);
197
+
198
+ const handleAction = useCallback(
199
+ async (action: ContextMenuEvent) => {
200
+ const item = getMessage();
201
+ if (!item) return;
202
+
203
+ switch (action.key) {
204
+ case 'edit': {
205
+ toggleMessageEditing(id, true);
206
+ break;
207
+ }
208
+ case 'copy': {
209
+ await copyMessage(id, item.content);
210
+ message.success(t('copySuccess'));
211
+ break;
212
+ }
213
+ case 'expand':
214
+ case 'collapse': {
215
+ toggleMessageCollapsed(id);
216
+ break;
217
+ }
218
+ case 'branching': {
219
+ if (!topic) {
220
+ message.warning(t('branchingRequiresSavedTopic'));
221
+ break;
222
+ }
223
+ openThreadCreator(id);
224
+ break;
225
+ }
226
+ case 'del': {
227
+ deleteMessage(id);
228
+ break;
229
+ }
230
+ case 'regenerate': {
231
+ if (inPortalThread) {
232
+ resendThreadMessage(id);
233
+ } else if (role === 'assistant') {
234
+ regenerateAssistantMessage(id);
235
+ } else {
236
+ regenerateUserMessage(id);
237
+ }
238
+
239
+ if (item.error) deleteMessage(id);
240
+ break;
241
+ }
242
+ case 'delAndRegenerate': {
243
+ if (inPortalThread) {
244
+ delAndResendThreadMessage(id);
245
+ } else {
246
+ delAndRegenerateMessage(id);
247
+ }
248
+ break;
249
+ }
250
+ case 'tts': {
251
+ ttsMessage(id);
252
+ break;
253
+ }
254
+ case 'share': {
255
+ handleShare();
256
+ break;
257
+ }
258
+ }
259
+
260
+ if (action.keyPath?.[0] === 'translate') {
261
+ const lang = action.keyPath.at(-1);
262
+ if (lang) translateMessage(id, lang);
263
+ }
264
+ },
265
+ [
266
+ copyMessage,
267
+ deleteMessage,
268
+ delAndRegenerateMessage,
269
+ delAndResendThreadMessage,
270
+ getMessage,
271
+ handleShare,
272
+ id,
273
+ inPortalThread,
274
+ message,
275
+ openThreadCreator,
276
+ regenerateAssistantMessage,
277
+ regenerateUserMessage,
278
+ resendThreadMessage,
279
+ role,
280
+ t,
281
+ toggleMessageCollapsed,
282
+ toggleMessageEditing,
283
+ topic,
284
+ translateMessage,
285
+ ttsMessage,
286
+ ],
287
+ );
288
+
289
+ const handleMenuClick = useCallback(
290
+ (info: ActionIconGroupEvent) => {
291
+ handleAction({
292
+ ...info,
293
+ selectedText: selectedTextRef.current,
294
+ } as ContextMenuEvent);
295
+ },
296
+ [handleAction],
297
+ );
298
+
299
+ const contextMenuItems = useMemo<GenericItemType[]>(() => {
300
+ if (!menuItems) return [];
301
+ return menuItems.filter(Boolean).map((item) => {
302
+ if ('type' in item && item.type === 'divider') return { type: 'divider' as const };
303
+
304
+ const actionItem = item as ActionMenuItem;
305
+ const children = actionItem.children?.map((child) => ({
306
+ key: child.key,
307
+ label: child.label,
308
+ onClick: handleMenuClick,
309
+ }));
310
+ const disabled =
311
+ actionItem.disabled ??
312
+ (typeof actionItem.disable === 'boolean' ? actionItem.disable : undefined);
313
+
314
+ return {
315
+ children,
316
+ danger: actionItem.danger,
317
+ disabled,
318
+ icon: actionItem.icon,
319
+ key: actionItem.key,
320
+ label: actionItem.label,
321
+ onClick: children ? undefined : handleMenuClick,
322
+ } satisfies DropdownItem;
323
+ });
324
+ }, [handleMenuClick, menuItems]);
33
325
 
34
326
  const handleContextMenu = useCallback(
35
- (event: React.MouseEvent) => {
36
- // Don't show context menu if disabled in settings
327
+ (event: MouseEvent<HTMLDivElement>) => {
37
328
  if (contextMenuMode === 'disabled') {
38
329
  return;
39
330
  }
40
331
 
41
- // Don't show context menu in editing mode
42
332
  if (editing) {
43
333
  return;
44
334
  }
45
335
 
46
- // Check if the clicked element or its parents have an id containing "msg_"
47
336
  let target = event.target as HTMLElement;
48
337
  let hasMessageId = false;
49
338
 
@@ -55,82 +344,23 @@ export const useChatItemContextMenu = ({
55
344
  target = target.parentElement as HTMLElement;
56
345
  }
57
346
 
58
- if (!hasMessageId) {
347
+ if (!hasMessageId || contextMenuItems.length === 0) {
59
348
  return;
60
349
  }
61
350
 
62
351
  event.preventDefault();
63
352
  event.stopPropagation();
64
353
 
65
- // Get selected text
66
354
  const selection = window.getSelection();
67
- const selectedText = selection?.toString().trim() || '';
68
-
69
- setContextMenuState({
70
- position: { x: event.clientX, y: event.clientY },
71
- selectedText,
72
- visible: true,
73
- });
74
- },
75
- [contextMenuMode, editing],
76
- );
77
-
78
- const hideContextMenu = useCallback(() => {
79
- setContextMenuState((prev) => ({ ...prev, visible: false }));
80
- }, []);
355
+ selectedTextRef.current = selection?.toString().trim() || '';
81
356
 
82
- const handleMenuClick = useCallback(
83
- (action: ActionIconGroupEvent) => {
84
- if (action.key === 'quote' && contextMenuState.selectedText) {
85
- // Handle quote action - this will be integrated with ChatInput
86
- onActionClick({
87
- ...action,
88
- selectedText: contextMenuState.selectedText,
89
- } as ActionIconGroupEvent & { selectedText: string });
90
- } else {
91
- onActionClick(action);
92
- }
93
- hideContextMenu();
357
+ console.log(contextMenuItems);
358
+ showContextMenu(contextMenuItems);
94
359
  },
95
- [contextMenuState.selectedText, onActionClick, hideContextMenu],
360
+ [contextMenuItems, contextMenuMode, editing],
96
361
  );
97
362
 
98
- // Close context menu when clicking outside
99
- useEffect(() => {
100
- const handleClickOutside = () => {
101
- if (contextMenuState.visible) {
102
- hideContextMenu();
103
- }
104
- };
105
-
106
- const handleScroll = (event: Event) => {
107
- if (contextMenuState.visible) {
108
- // Check if the scroll event is from a dropdown sub-menu
109
- const target = event.target as HTMLElement;
110
- if (target && target.classList && target.classList.contains('ant-dropdown-menu-sub')) {
111
- return; // Don't hide the context menu when scrolling within sub-menu
112
- }
113
- hideContextMenu();
114
- }
115
- };
116
-
117
- if (contextMenuState.visible) {
118
- document.addEventListener('click', handleClickOutside);
119
- document.addEventListener('scroll', handleScroll, true);
120
-
121
- return () => {
122
- document.removeEventListener('click', handleClickOutside);
123
- document.removeEventListener('scroll', handleScroll, true);
124
- };
125
- }
126
- }, [contextMenuState.visible, hideContextMenu]);
127
-
128
363
  return {
129
- containerRef,
130
- contextMenuMode,
131
- contextMenuState,
132
364
  handleContextMenu,
133
- handleMenuClick,
134
- hideContextMenu,
135
365
  };
136
366
  };
@@ -1,6 +1,16 @@
1
1
  'use client';
2
2
 
3
- import { Block, type BlockProps, Center, Flexbox, Icon, type IconProps, Text } from '@lobehub/ui';
3
+ import {
4
+ Block,
5
+ type BlockProps,
6
+ Center,
7
+ ContextMenuTrigger,
8
+ Flexbox,
9
+ type GenericItemType,
10
+ Icon,
11
+ type IconProps,
12
+ Text,
13
+ } from '@lobehub/ui';
4
14
  import { createStaticStyles, cssVar, cx } from 'antd-style';
5
15
  import { Loader2Icon } from 'lucide-react';
6
16
  import { type ReactNode, memo } from 'react';
@@ -18,6 +28,11 @@ const styles = createStaticStyles(({ css }) => ({
18
28
  margin-inline-end: 2px;
19
29
  opacity: 0;
20
30
  transition: opacity 0.2s ${cssVar.motionEaseOut};
31
+
32
+ &:has([data-popup-open]) {
33
+ width: unset;
34
+ opacity: 1;
35
+ }
21
36
  }
22
37
 
23
38
  &:hover {
@@ -32,6 +47,7 @@ const styles = createStaticStyles(({ css }) => ({
32
47
  export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
33
48
  actions?: ReactNode;
34
49
  active?: boolean;
50
+ contextMenuItems?: GenericItemType[] | (() => GenericItemType[]);
35
51
  disabled?: boolean;
36
52
  extra?: ReactNode;
37
53
  icon?: IconProps['icon'];
@@ -40,13 +56,25 @@ export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
40
56
  }
41
57
 
42
58
  const NavItem = memo<NavItemProps>(
43
- ({ className, actions, active, icon, title, onClick, disabled, loading, extra, ...rest }) => {
59
+ ({
60
+ className,
61
+ actions,
62
+ contextMenuItems,
63
+ active,
64
+ icon,
65
+ title,
66
+ onClick,
67
+ disabled,
68
+ loading,
69
+ extra,
70
+ ...rest
71
+ }) => {
44
72
  const iconColor = active ? cssVar.colorText : cssVar.colorTextDescription;
45
73
  const textColor = active ? cssVar.colorText : cssVar.colorTextSecondary;
46
74
  const variant = active ? 'filled' : 'borderless';
47
75
  const iconComponent = loading ? Loader2Icon : icon;
48
76
 
49
- return (
77
+ const Content = (
50
78
  <Block
51
79
  align={'center'}
52
80
  className={cx(styles.container, className)}
@@ -102,6 +130,8 @@ const NavItem = memo<NavItemProps>(
102
130
  </Flexbox>
103
131
  </Block>
104
132
  );
133
+ if (!contextMenuItems) return Content;
134
+ return <ContextMenuTrigger items={contextMenuItems}>{Content}</ContextMenuTrigger>;
105
135
  },
106
136
  );
107
137
 
@@ -1,27 +1,19 @@
1
- import { ActionIcon, Dropdown, type MenuProps } from '@lobehub/ui';
1
+ import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
2
2
  import { MoreHorizontalIcon } from 'lucide-react';
3
3
  import { memo } from 'react';
4
4
 
5
5
  interface ActionsProps {
6
- dropdownMenu: MenuProps['items'];
6
+ dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
7
7
  }
8
8
 
9
9
  const Actions = memo<ActionsProps>(({ dropdownMenu }) => {
10
- if (!dropdownMenu || dropdownMenu.length === 0) return null;
10
+ if (!dropdownMenu || (typeof dropdownMenu !== 'function' && dropdownMenu.length === 0))
11
+ return null;
11
12
 
12
13
  return (
13
- <Dropdown
14
- arrow={false}
15
- menu={{
16
- items: dropdownMenu,
17
- onClick: ({ domEvent }) => {
18
- domEvent.stopPropagation();
19
- },
20
- }}
21
- trigger={['click']}
22
- >
14
+ <DropdownMenu items={dropdownMenu}>
23
15
  <ActionIcon icon={MoreHorizontalIcon} size={'small'} />
24
- </Dropdown>
16
+ </DropdownMenu>
25
17
  );
26
18
  });
27
19
 
@@ -28,6 +28,7 @@ const TopicItem = memo<TopicItemProps>(
28
28
  <NavItem
29
29
  actions={<Actions dropdownMenu={dropdownMenu} />}
30
30
  active={active}
31
+ contextMenuItems={dropdownMenu}
31
32
  onClick={() => {
32
33
  onTopicChange(topicId);
33
34
  onClose();
@@ -1,7 +1,7 @@
1
1
  import { Icon, type MenuProps } from '@lobehub/ui';
2
2
  import { App } from 'antd';
3
3
  import { Trash2 } from 'lucide-react';
4
- import { useMemo } from 'react';
4
+ import { useCallback } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useChatStore } from '@/store/chat';
@@ -12,7 +12,10 @@ interface UseDropdownMenuProps {
12
12
  topicTitle: string;
13
13
  }
14
14
 
15
- export const useDropdownMenu = ({ onClose, topicId }: UseDropdownMenuProps): MenuProps['items'] => {
15
+ export const useDropdownMenu = ({
16
+ onClose,
17
+ topicId,
18
+ }: UseDropdownMenuProps): (() => MenuProps['items']) => {
16
19
  const { t } = useTranslation(['common', 'topic']);
17
20
  const { modal } = App.useApp();
18
21
  const removeTopic = useChatStore((s) => s.removeTopic);
@@ -32,7 +35,7 @@ export const useDropdownMenu = ({ onClose, topicId }: UseDropdownMenuProps): Men
32
35
  });
33
36
  };
34
37
 
35
- return useMemo(
38
+ return useCallback(
36
39
  () =>
37
40
  [
38
41
  {
@@ -1,42 +1,19 @@
1
- import { ActionIcon, Dropdown } from '@lobehub/ui';
1
+ import { ActionIcon, DropdownMenu as DropdownMenuUI } from '@lobehub/ui';
2
+ import { type ItemType } from 'antd/es/menu/interface';
2
3
  import { MoreHorizontalIcon } from 'lucide-react';
3
- import { memo, useState } from 'react';
4
-
5
- import { useFileItemDropdown } from './useFileItemDropdown';
4
+ import { memo } from 'react';
6
5
 
7
6
  interface DropdownMenuProps {
8
- fileType: string;
9
- filename: string;
10
- id: string;
11
- knowledgeBaseId?: string;
12
- onRenameStart?: () => void;
13
- sourceType?: string;
14
- url: string;
7
+ className?: string;
8
+ items: ItemType[] | (() => ItemType[]);
15
9
  }
16
10
 
17
- const DropdownMenu = memo<DropdownMenuProps>(
18
- ({ id, knowledgeBaseId, url, filename, fileType, sourceType, onRenameStart }) => {
19
- const [isOpen, setIsOpen] = useState(false);
20
-
21
- // Only compute dropdown items when dropdown is actually open
22
- // This prevents expensive hook execution for all 20-25 visible items
23
- const { menuItems } = useFileItemDropdown({
24
- enabled: isOpen,
25
- fileType,
26
- filename,
27
- id,
28
- knowledgeBaseId,
29
- onRenameStart,
30
- sourceType,
31
- url,
32
- });
33
-
34
- return (
35
- <Dropdown menu={{ items: menuItems }} onOpenChange={setIsOpen} open={isOpen}>
36
- <ActionIcon icon={MoreHorizontalIcon} size={'small'} />
37
- </Dropdown>
38
- );
39
- },
40
- );
11
+ const DropdownMenu = memo<DropdownMenuProps>(({ items, className }) => {
12
+ return (
13
+ <DropdownMenuUI items={items}>
14
+ <ActionIcon className={className} icon={MoreHorizontalIcon} size={'small'} />
15
+ </DropdownMenuUI>
16
+ );
17
+ });
41
18
 
42
19
  export default DropdownMenu;
@@ -10,7 +10,7 @@ import {
10
10
  PencilIcon,
11
11
  Trash,
12
12
  } from 'lucide-react';
13
- import { useMemo } from 'react';
13
+ import { useCallback } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
 
16
16
  import RepoIcon from '@/components/LibIcon';
@@ -33,11 +33,10 @@ interface UseFileItemDropdownParams {
33
33
  }
34
34
 
35
35
  interface UseFileItemDropdownReturn {
36
- menuItems: ItemType[];
36
+ menuItems: () => ItemType[];
37
37
  }
38
38
 
39
39
  export const useFileItemDropdown = ({
40
- enabled = true,
41
40
  id,
42
41
  knowledgeBaseId,
43
42
  url,
@@ -65,10 +64,7 @@ export const useFileItemDropdown = ({
65
64
  const isFolder = fileType === 'custom/folder';
66
65
  const isPage = sourceType === 'document' || fileType === 'custom/document';
67
66
 
68
- const menuItems = useMemo(() => {
69
- // Skip building menu items when dropdown is not open (performance optimization)
70
- if (!enabled) return [];
71
-
67
+ const menuItems = useCallback(() => {
72
68
  // Filter out current knowledge base and create submenu items
73
69
  const availableKnowledgeBases = (knowledgeBases || []).filter(
74
70
  (kb) => kb.id !== knowledgeBaseId,
@@ -264,7 +260,7 @@ export const useFileItemDropdown = ({
264
260
  },
265
261
  ] as ItemType[]
266
262
  ).filter(Boolean);
267
- }, [enabled, inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
263
+ }, [inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
268
264
 
269
265
  return { menuItems };
270
266
  };