@lobehub/lobehub 2.0.0-next.109 → 2.0.0-next.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/apps/desktop/src/common/routes.ts +0 -6
- package/apps/desktop/src/main/appBrowsers.ts +0 -13
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +29 -48
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +21 -72
- package/apps/desktop/src/main/core/browser/Browser.ts +1 -0
- package/apps/desktop/src/main/core/browser/BrowserManager.ts +1 -56
- package/apps/desktop/src/main/menus/impls/macOS.ts +9 -3
- package/changelog/v1.json +9 -0
- package/locales/ar/setting.json +7 -1
- package/locales/bg-BG/setting.json +7 -1
- package/locales/de-DE/setting.json +7 -1
- package/locales/en-US/setting.json +7 -1
- package/locales/es-ES/setting.json +7 -1
- package/locales/fa-IR/setting.json +7 -1
- package/locales/fr-FR/setting.json +7 -1
- package/locales/it-IT/setting.json +7 -1
- package/locales/ja-JP/setting.json +7 -1
- package/locales/ko-KR/setting.json +7 -1
- package/locales/nl-NL/setting.json +7 -1
- package/locales/pl-PL/setting.json +7 -1
- package/locales/pt-BR/setting.json +7 -1
- package/locales/ru-RU/setting.json +7 -1
- package/locales/tr-TR/setting.json +7 -1
- package/locales/vi-VN/setting.json +7 -1
- package/locales/zh-CN/setting.json +6 -0
- package/locales/zh-TW/setting.json +7 -1
- package/package.json +1 -1
- package/packages/const/src/settings/common.ts +1 -0
- package/packages/model-bank/src/aiModels/ollamacloud.ts +0 -1
- package/packages/types/src/user/settings/general.ts +3 -0
- package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/index.tsx +32 -18
- package/src/app/[variants]/(main)/layouts/desktop/DesktopLayoutContainer.tsx +3 -6
- package/src/app/[variants]/(main)/layouts/desktop/SideBar/PinList/index.tsx +21 -14
- package/src/app/[variants]/(main)/settings/common/features/Common/Common.tsx +23 -1
- package/src/features/ChatItem/components/MessageContent.tsx +2 -1
- package/src/features/ChatList/Messages/Assistant/Actions/index.tsx +1 -0
- package/src/features/ChatList/Messages/Assistant/index.tsx +1 -1
- package/src/features/ChatList/Messages/Default.tsx +2 -0
- package/src/features/ChatList/Messages/index.tsx +80 -31
- package/src/features/ChatList/components/ContextMenu.tsx +391 -0
- package/src/features/ChatList/hooks/useChatItemContextMenu.tsx +135 -0
- package/src/locales/default/setting.ts +6 -0
- package/src/store/user/slices/settings/selectors/general.ts +8 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { UIChatMessage } from '@lobechat/types';
|
|
4
|
+
import { ActionIcon } from '@lobehub/ui';
|
|
5
|
+
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
|
|
6
|
+
import { Dropdown, type MenuProps } from 'antd';
|
|
7
|
+
import { App } from 'antd';
|
|
8
|
+
import { createStyles } from 'antd-style';
|
|
9
|
+
import isEqual from 'fast-deep-equal';
|
|
10
|
+
import { isValidElement, memo, useCallback, useMemo, useState, type RefObject , ComponentType, ReactNode } from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import type { VListHandle } from 'virtua';
|
|
14
|
+
|
|
15
|
+
import ShareMessageModal from './ShareMessageModal';
|
|
16
|
+
import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
|
|
17
|
+
import { getChatStoreState, useChatStore } from '@/store/chat';
|
|
18
|
+
import {
|
|
19
|
+
displayMessageSelectors,
|
|
20
|
+
messageStateSelectors,
|
|
21
|
+
threadSelectors,
|
|
22
|
+
} from '@/store/chat/selectors';
|
|
23
|
+
import { useSessionStore } from '@/store/session';
|
|
24
|
+
import { sessionSelectors } from '@/store/session/selectors';
|
|
25
|
+
|
|
26
|
+
interface ActionMenuItem extends ActionIconGroupItemType {
|
|
27
|
+
children?: { key: string; label: ReactNode }[];
|
|
28
|
+
disable?: boolean;
|
|
29
|
+
popupClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MenuItem = ActionMenuItem | { type: 'divider' };
|
|
33
|
+
type ContextMenuEvent = ActionIconGroupEvent & { selectedText?: string };
|
|
34
|
+
|
|
35
|
+
const useStyles = createStyles(({ css }) => ({
|
|
36
|
+
contextMenu: css`
|
|
37
|
+
position: fixed;
|
|
38
|
+
z-index: 1000;
|
|
39
|
+
min-width: 160px;
|
|
40
|
+
|
|
41
|
+
.ant-dropdown-menu {
|
|
42
|
+
border: none;
|
|
43
|
+
border-radius: 6px;
|
|
44
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
trigger: css`
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
|
|
50
|
+
position: fixed;
|
|
51
|
+
|
|
52
|
+
width: 1px;
|
|
53
|
+
height: 1px;
|
|
54
|
+
|
|
55
|
+
opacity: 0;
|
|
56
|
+
`,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
interface ContextMenuProps {
|
|
60
|
+
id: string;
|
|
61
|
+
inPortalThread: boolean;
|
|
62
|
+
index: number;
|
|
63
|
+
onClose: () => void;
|
|
64
|
+
position: { x: number; y: number };
|
|
65
|
+
selectedText?: string;
|
|
66
|
+
topic: string | null;
|
|
67
|
+
virtuaRef?: RefObject<VListHandle | null> | null;
|
|
68
|
+
visible: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ContextMenu = memo<ContextMenuProps>(
|
|
72
|
+
({ visible, position, selectedText, id, index, inPortalThread, topic, virtuaRef, onClose }) => {
|
|
73
|
+
const { styles } = useStyles();
|
|
74
|
+
const { message } = App.useApp();
|
|
75
|
+
const { t } = useTranslation('common');
|
|
76
|
+
const [shareMessage, setShareMessage] = useState<UIChatMessage | null>(null);
|
|
77
|
+
const [isShareModalOpen, setShareModalOpen] = useState(false);
|
|
78
|
+
|
|
79
|
+
const [role, error, isCollapsed, hasThread, isRegenerating] = useChatStore(
|
|
80
|
+
(s) => {
|
|
81
|
+
const item = displayMessageSelectors.getDisplayMessageById(id)(s);
|
|
82
|
+
return [
|
|
83
|
+
item?.role,
|
|
84
|
+
item?.error,
|
|
85
|
+
messageStateSelectors.isMessageCollapsed(id)(s),
|
|
86
|
+
threadSelectors.hasThreadBySourceMsgId(id)(s),
|
|
87
|
+
messageStateSelectors.isMessageRegenerating(id)(s),
|
|
88
|
+
];
|
|
89
|
+
},
|
|
90
|
+
isEqual,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const isThreadMode = useChatStore((s) => !!s.activeThreadId);
|
|
94
|
+
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
|
95
|
+
const actionsBar = useChatListActionsBar({ hasThread, isRegenerating });
|
|
96
|
+
const inThread = isThreadMode || inPortalThread;
|
|
97
|
+
|
|
98
|
+
const [
|
|
99
|
+
toggleMessageEditing,
|
|
100
|
+
deleteMessage,
|
|
101
|
+
regenerateUserMessage,
|
|
102
|
+
regenerateAssistantMessage,
|
|
103
|
+
translateMessage,
|
|
104
|
+
ttsMessage,
|
|
105
|
+
delAndRegenerateMessage,
|
|
106
|
+
copyMessage,
|
|
107
|
+
openThreadCreator,
|
|
108
|
+
resendThreadMessage,
|
|
109
|
+
delAndResendThreadMessage,
|
|
110
|
+
toggleMessageCollapsed,
|
|
111
|
+
] = useChatStore((s) => [
|
|
112
|
+
s.toggleMessageEditing,
|
|
113
|
+
s.deleteMessage,
|
|
114
|
+
s.regenerateUserMessage,
|
|
115
|
+
s.regenerateAssistantMessage,
|
|
116
|
+
s.translateMessage,
|
|
117
|
+
s.ttsMessage,
|
|
118
|
+
s.delAndRegenerateMessage,
|
|
119
|
+
s.copyMessage,
|
|
120
|
+
s.openThreadCreator,
|
|
121
|
+
s.resendThreadMessage,
|
|
122
|
+
s.delAndResendThreadMessage,
|
|
123
|
+
s.toggleMessageCollapsed,
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const getMessage = useCallback(
|
|
127
|
+
() => displayMessageSelectors.getDisplayMessageById(id)(getChatStoreState()),
|
|
128
|
+
[id],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const menuItems = useMemo<MenuItem[]>(() => {
|
|
132
|
+
if (!role) return [];
|
|
133
|
+
|
|
134
|
+
const {
|
|
135
|
+
branching,
|
|
136
|
+
collapse,
|
|
137
|
+
copy,
|
|
138
|
+
del,
|
|
139
|
+
delAndRegenerate,
|
|
140
|
+
divider,
|
|
141
|
+
edit,
|
|
142
|
+
expand,
|
|
143
|
+
regenerate,
|
|
144
|
+
share,
|
|
145
|
+
translate,
|
|
146
|
+
tts,
|
|
147
|
+
} = actionsBar;
|
|
148
|
+
|
|
149
|
+
if (role === 'assistant') {
|
|
150
|
+
if (error) {
|
|
151
|
+
return [edit, copy, divider, del, divider, regenerate].filter(Boolean) as MenuItem[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const collapseAction = isCollapsed ? expand : collapse;
|
|
155
|
+
const list: MenuItem[] = [edit, copy, collapseAction];
|
|
156
|
+
|
|
157
|
+
if (!inThread && !isGroupSession) list.push(branching);
|
|
158
|
+
|
|
159
|
+
list.push(
|
|
160
|
+
divider,
|
|
161
|
+
tts,
|
|
162
|
+
translate,
|
|
163
|
+
divider,
|
|
164
|
+
share,
|
|
165
|
+
divider,
|
|
166
|
+
regenerate,
|
|
167
|
+
delAndRegenerate,
|
|
168
|
+
del,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return list.filter(Boolean) as MenuItem[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (role === 'user') {
|
|
175
|
+
const list: MenuItem[] = [edit, copy];
|
|
176
|
+
|
|
177
|
+
if (!inThread) list.push(branching);
|
|
178
|
+
|
|
179
|
+
list.push(divider, tts, translate, divider, regenerate, del);
|
|
180
|
+
|
|
181
|
+
return list.filter(Boolean) as MenuItem[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return [];
|
|
185
|
+
}, [actionsBar, error, inThread, isCollapsed, isGroupSession, role]);
|
|
186
|
+
|
|
187
|
+
const handleShare = useCallback(() => {
|
|
188
|
+
const item = getMessage();
|
|
189
|
+
if (!item || item.role !== 'assistant') return;
|
|
190
|
+
|
|
191
|
+
setShareMessage(item);
|
|
192
|
+
setShareModalOpen(true);
|
|
193
|
+
}, [getMessage]);
|
|
194
|
+
|
|
195
|
+
const handleShareClose = useCallback(() => {
|
|
196
|
+
setShareModalOpen(false);
|
|
197
|
+
setShareMessage(null);
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
const handleAction = useCallback(
|
|
201
|
+
async (action: ContextMenuEvent) => {
|
|
202
|
+
const item = getMessage();
|
|
203
|
+
if (!item) return;
|
|
204
|
+
|
|
205
|
+
if (action.key === 'edit') {
|
|
206
|
+
toggleMessageEditing(id, true);
|
|
207
|
+
virtuaRef?.current?.scrollToIndex(index, { align: 'start' });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
switch (action.key) {
|
|
211
|
+
case 'copy': {
|
|
212
|
+
await copyMessage(id, item.content);
|
|
213
|
+
message.success(t('copySuccess'));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'expand':
|
|
217
|
+
case 'collapse': {
|
|
218
|
+
toggleMessageCollapsed(id);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case 'branching': {
|
|
222
|
+
if (!topic) {
|
|
223
|
+
message.warning(t('branchingRequiresSavedTopic'));
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
openThreadCreator(id);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'del': {
|
|
230
|
+
deleteMessage(id);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'regenerate': {
|
|
234
|
+
if (inPortalThread) {
|
|
235
|
+
resendThreadMessage(id);
|
|
236
|
+
} else if (role === 'assistant') {
|
|
237
|
+
regenerateAssistantMessage(id);
|
|
238
|
+
} else {
|
|
239
|
+
regenerateUserMessage(id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (item.error) deleteMessage(id);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'delAndRegenerate': {
|
|
246
|
+
if (inPortalThread) {
|
|
247
|
+
delAndResendThreadMessage(id);
|
|
248
|
+
} else {
|
|
249
|
+
delAndRegenerateMessage(id);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'tts': {
|
|
254
|
+
ttsMessage(id);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'share': {
|
|
258
|
+
handleShare();
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (action.keyPath?.at(-1) === 'translate') {
|
|
264
|
+
const lang = action.keyPath[0];
|
|
265
|
+
translateMessage(id, lang);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[
|
|
269
|
+
copyMessage,
|
|
270
|
+
deleteMessage,
|
|
271
|
+
delAndRegenerateMessage,
|
|
272
|
+
delAndResendThreadMessage,
|
|
273
|
+
getMessage,
|
|
274
|
+
handleShare,
|
|
275
|
+
id,
|
|
276
|
+
index,
|
|
277
|
+
inPortalThread,
|
|
278
|
+
message,
|
|
279
|
+
openThreadCreator,
|
|
280
|
+
regenerateAssistantMessage,
|
|
281
|
+
regenerateUserMessage,
|
|
282
|
+
resendThreadMessage,
|
|
283
|
+
role,
|
|
284
|
+
t,
|
|
285
|
+
toggleMessageCollapsed,
|
|
286
|
+
toggleMessageEditing,
|
|
287
|
+
topic,
|
|
288
|
+
translateMessage,
|
|
289
|
+
ttsMessage,
|
|
290
|
+
virtuaRef,
|
|
291
|
+
],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const renderIcon = useCallback((iconComponent: ActionIconGroupItemType['icon']) => {
|
|
295
|
+
if (!iconComponent) return null;
|
|
296
|
+
|
|
297
|
+
if (isValidElement(iconComponent)) {
|
|
298
|
+
return <ActionIcon icon={iconComponent} size={'small'} />;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const IconComponent = iconComponent as ComponentType<{ size?: number }>;
|
|
302
|
+
|
|
303
|
+
return <ActionIcon icon={<IconComponent size={16} />} size={'small'} />;
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
const dropdownMenuItems = useMemo(() => {
|
|
307
|
+
return (menuItems ?? []).filter(Boolean).map((item) => {
|
|
308
|
+
if ('type' in item && item.type === 'divider') return { type: 'divider' as const };
|
|
309
|
+
|
|
310
|
+
const actionItem = item as ActionMenuItem;
|
|
311
|
+
const children = actionItem.children?.map((child) => ({
|
|
312
|
+
key: child.key,
|
|
313
|
+
label: child.label,
|
|
314
|
+
}));
|
|
315
|
+
const disabled =
|
|
316
|
+
actionItem.disabled ??
|
|
317
|
+
(typeof actionItem.disable === 'boolean' ? actionItem.disable : undefined);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
children,
|
|
321
|
+
danger: actionItem.danger,
|
|
322
|
+
disabled,
|
|
323
|
+
icon: renderIcon(actionItem.icon),
|
|
324
|
+
key: actionItem.key,
|
|
325
|
+
label: actionItem.label,
|
|
326
|
+
popupClassName: actionItem.popupClassName,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}, [menuItems, renderIcon]);
|
|
330
|
+
|
|
331
|
+
const handleMenuClick = useCallback(
|
|
332
|
+
(info: Parameters<NonNullable<MenuProps['onClick']>>[0]) => {
|
|
333
|
+
const event = {
|
|
334
|
+
...info,
|
|
335
|
+
selectedText,
|
|
336
|
+
} as ContextMenuEvent;
|
|
337
|
+
|
|
338
|
+
handleAction(event);
|
|
339
|
+
onClose();
|
|
340
|
+
},
|
|
341
|
+
[handleAction, onClose, selectedText],
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
if (!visible || menuItems.length === 0) return null;
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<>
|
|
348
|
+
{createPortal(
|
|
349
|
+
<>
|
|
350
|
+
<div
|
|
351
|
+
className={styles.trigger}
|
|
352
|
+
style={{
|
|
353
|
+
left: position.x,
|
|
354
|
+
top: position.y,
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
<Dropdown
|
|
358
|
+
menu={{
|
|
359
|
+
items: dropdownMenuItems,
|
|
360
|
+
onClick: handleMenuClick,
|
|
361
|
+
}}
|
|
362
|
+
open={visible}
|
|
363
|
+
placement="bottomLeft"
|
|
364
|
+
trigger={[]}
|
|
365
|
+
>
|
|
366
|
+
<div
|
|
367
|
+
className={styles.contextMenu}
|
|
368
|
+
style={{
|
|
369
|
+
left: position.x,
|
|
370
|
+
top: position.y,
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
</Dropdown>
|
|
374
|
+
</>,
|
|
375
|
+
document.body,
|
|
376
|
+
)}
|
|
377
|
+
{shareMessage && (
|
|
378
|
+
<ShareMessageModal
|
|
379
|
+
message={shareMessage}
|
|
380
|
+
onCancel={handleShareClose}
|
|
381
|
+
open={isShareModalOpen}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
</>
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
ContextMenu.displayName = 'ContextMenu';
|
|
390
|
+
|
|
391
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { type ActionIconGroupEvent } from '@lobehub/ui';
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useUserStore } from '@/store/user';
|
|
5
|
+
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
|
6
|
+
|
|
7
|
+
import { MessageContentClassName } from '../Messages/Default';
|
|
8
|
+
|
|
9
|
+
interface ContextMenuState {
|
|
10
|
+
position: { x: number; y: number };
|
|
11
|
+
selectedText?: string;
|
|
12
|
+
visible: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseChatItemContextMenuProps {
|
|
16
|
+
editing?: boolean;
|
|
17
|
+
id: string;
|
|
18
|
+
onActionClick: (action: ActionIconGroupEvent) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useChatItemContextMenu = ({
|
|
22
|
+
onActionClick,
|
|
23
|
+
editing,
|
|
24
|
+
}: Omit<UseChatItemContextMenuProps, 'id'>) => {
|
|
25
|
+
const contextMenuMode = useUserStore(userGeneralSettingsSelectors.contextMenuMode);
|
|
26
|
+
|
|
27
|
+
const [contextMenuState, setContextMenuState] = useState<ContextMenuState>({
|
|
28
|
+
position: { x: 0, y: 0 },
|
|
29
|
+
visible: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
|
|
34
|
+
const handleContextMenu = useCallback(
|
|
35
|
+
(event: React.MouseEvent) => {
|
|
36
|
+
// Don't show context menu if disabled in settings
|
|
37
|
+
if (contextMenuMode === 'disabled') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Don't show context menu in editing mode
|
|
42
|
+
if (editing) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if the clicked element or its parents have an id containing "msg_"
|
|
47
|
+
let target = event.target as HTMLElement;
|
|
48
|
+
let hasMessageId = false;
|
|
49
|
+
|
|
50
|
+
while (target && target !== document.body) {
|
|
51
|
+
if (target.className.includes(MessageContentClassName)) {
|
|
52
|
+
hasMessageId = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
target = target.parentElement as HTMLElement;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!hasMessageId) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
|
|
65
|
+
// Get selected text
|
|
66
|
+
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
|
+
}, []);
|
|
81
|
+
|
|
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();
|
|
94
|
+
},
|
|
95
|
+
[contextMenuState.selectedText, onActionClick, hideContextMenu],
|
|
96
|
+
);
|
|
97
|
+
|
|
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
|
+
return {
|
|
129
|
+
containerRef,
|
|
130
|
+
contextMenuState,
|
|
131
|
+
handleContextMenu,
|
|
132
|
+
handleMenuClick,
|
|
133
|
+
hideContextMenu,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
@@ -297,6 +297,12 @@ export default {
|
|
|
297
297
|
elegant: '优雅',
|
|
298
298
|
title: '响应动画',
|
|
299
299
|
},
|
|
300
|
+
contextMenuMode: {
|
|
301
|
+
default: '默认',
|
|
302
|
+
desc: '选择聊天消息右键菜单的显示方案',
|
|
303
|
+
disabled: '不使用',
|
|
304
|
+
title: '右键菜单方案',
|
|
305
|
+
},
|
|
300
306
|
neutralColor: {
|
|
301
307
|
desc: '不同色彩倾向的灰阶自定义',
|
|
302
308
|
title: '中性色',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isDesktop } from '@lobechat/const';
|
|
2
|
+
|
|
1
3
|
import { UserStore } from '../../../store';
|
|
2
4
|
import { currentSettings } from './settings';
|
|
3
5
|
|
|
@@ -10,10 +12,16 @@ const highlighterTheme = (s: UserStore) => generalConfig(s).highlighterTheme;
|
|
|
10
12
|
const mermaidTheme = (s: UserStore) => generalConfig(s).mermaidTheme;
|
|
11
13
|
const transitionMode = (s: UserStore) => generalConfig(s).transitionMode;
|
|
12
14
|
const animationMode = (s: UserStore) => generalConfig(s).animationMode;
|
|
15
|
+
const contextMenuMode = (s: UserStore) => {
|
|
16
|
+
const config = generalConfig(s).contextMenuMode;
|
|
17
|
+
if (config !== undefined) return config;
|
|
18
|
+
return isDesktop ? 'default' : 'disabled';
|
|
19
|
+
};
|
|
13
20
|
|
|
14
21
|
export const userGeneralSettingsSelectors = {
|
|
15
22
|
animationMode,
|
|
16
23
|
config: generalConfig,
|
|
24
|
+
contextMenuMode,
|
|
17
25
|
fontSize,
|
|
18
26
|
highlighterTheme,
|
|
19
27
|
mermaidTheme,
|