@opensumi/ide-ai-native 3.8.3-next-1742180589.0 → 3.8.3-next-1742193440.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.
- package/lib/browser/chat/chat-manager.service.d.ts +1 -1
- package/lib/browser/chat/chat-manager.service.d.ts.map +1 -1
- package/lib/browser/chat/chat-manager.service.js +3 -2
- package/lib/browser/chat/chat-manager.service.js.map +1 -1
- package/lib/browser/chat/chat-model.d.ts.map +1 -1
- package/lib/browser/chat/chat-model.js +11 -2
- package/lib/browser/chat/chat-model.js.map +1 -1
- package/lib/browser/chat/chat-proxy.service.d.ts.map +1 -1
- package/lib/browser/chat/chat-proxy.service.js +1 -0
- package/lib/browser/chat/chat-proxy.service.js.map +1 -1
- package/lib/browser/chat/chat.feature.registry.d.ts +4 -1
- package/lib/browser/chat/chat.feature.registry.d.ts.map +1 -1
- package/lib/browser/chat/chat.feature.registry.js +6 -0
- package/lib/browser/chat/chat.feature.registry.js.map +1 -1
- package/lib/browser/chat/chat.internal.service.d.ts +1 -1
- package/lib/browser/chat/chat.internal.service.d.ts.map +1 -1
- package/lib/browser/chat/chat.internal.service.js +2 -2
- package/lib/browser/chat/chat.internal.service.js.map +1 -1
- package/lib/browser/chat/chat.view.d.ts.map +1 -1
- package/lib/browser/chat/chat.view.js +10 -7
- package/lib/browser/chat/chat.view.js.map +1 -1
- package/lib/browser/components/ChatEditor.d.ts +2 -1
- package/lib/browser/components/ChatEditor.d.ts.map +1 -1
- package/lib/browser/components/ChatEditor.js +5 -2
- package/lib/browser/components/ChatEditor.js.map +1 -1
- package/lib/browser/components/ChatInput.d.ts +1 -1
- package/lib/browser/components/ChatInput.d.ts.map +1 -1
- package/lib/browser/components/ChatInput.js +1 -1
- package/lib/browser/components/ChatInput.js.map +1 -1
- package/lib/browser/components/ChatMentionInput.d.ts +3 -1
- package/lib/browser/components/ChatMentionInput.d.ts.map +1 -1
- package/lib/browser/components/ChatMentionInput.js +54 -3
- package/lib/browser/components/ChatMentionInput.js.map +1 -1
- package/lib/browser/components/components.module.less +49 -0
- package/lib/browser/components/mention-input/mention-input.d.ts.map +1 -1
- package/lib/browser/components/mention-input/mention-input.js +124 -60
- package/lib/browser/components/mention-input/mention-input.js.map +1 -1
- package/lib/browser/components/mention-input/types.d.ts +3 -1
- package/lib/browser/components/mention-input/types.d.ts.map +1 -1
- package/lib/browser/components/mention-input/types.js.map +1 -1
- package/lib/browser/context/llm-context.service.d.ts.map +1 -1
- package/lib/browser/context/llm-context.service.js.map +1 -1
- package/lib/browser/mcp/base-apply.service.d.ts.map +1 -1
- package/lib/browser/mcp/base-apply.service.js +5 -6
- package/lib/browser/mcp/base-apply.service.js.map +1 -1
- package/lib/browser/mcp/config/components/mcp-config.view.js +3 -3
- package/lib/browser/mcp/config/components/mcp-config.view.js.map +1 -1
- package/lib/browser/model/msg-history-manager.d.ts +1 -1
- package/lib/browser/model/msg-history-manager.d.ts.map +1 -1
- package/lib/browser/model/msg-history-manager.js.map +1 -1
- package/lib/browser/types.d.ts +7 -1
- package/lib/browser/types.d.ts.map +1 -1
- package/lib/browser/types.js.map +1 -1
- package/lib/common/index.d.ts +6 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js.map +1 -1
- package/lib/common/llm-context.d.ts.map +1 -1
- package/lib/common/llm-context.js.map +1 -1
- package/lib/node/base-language-model.d.ts +1 -1
- package/lib/node/base-language-model.d.ts.map +1 -1
- package/lib/node/base-language-model.js +15 -4
- package/lib/node/base-language-model.js.map +1 -1
- package/package.json +23 -23
- package/src/browser/chat/chat-manager.service.ts +3 -2
- package/src/browser/chat/chat-model.ts +10 -2
- package/src/browser/chat/chat-proxy.service.ts +1 -0
- package/src/browser/chat/chat.feature.registry.ts +10 -1
- package/src/browser/chat/chat.internal.service.ts +2 -2
- package/src/browser/chat/chat.view.tsx +18 -8
- package/src/browser/components/ChatEditor.tsx +8 -0
- package/src/browser/components/ChatInput.tsx +2 -2
- package/src/browser/components/ChatMentionInput.tsx +100 -6
- package/src/browser/components/components.module.less +49 -0
- package/src/browser/components/mention-input/mention-input.tsx +145 -64
- package/src/browser/components/mention-input/types.ts +3 -1
- package/src/browser/context/llm-context.service.ts +2 -0
- package/src/browser/mcp/base-apply.service.ts +7 -6
- package/src/browser/mcp/config/components/mcp-config.view.tsx +3 -3
- package/src/browser/model/msg-history-manager.ts +1 -1
- package/src/browser/types.ts +8 -1
- package/src/common/index.ts +6 -0
- package/src/common/llm-context.ts +2 -0
- package/src/node/base-language-model.ts +25 -3
|
@@ -281,7 +281,7 @@ export const AIChatView = () => {
|
|
|
281
281
|
if (loading) {
|
|
282
282
|
return;
|
|
283
283
|
}
|
|
284
|
-
await handleSend(message.message, message.agentId, message.command);
|
|
284
|
+
await handleSend(message.message, message.images, message.agentId, message.command);
|
|
285
285
|
} else {
|
|
286
286
|
if (message.agentId) {
|
|
287
287
|
setAgentId(message.agentId);
|
|
@@ -462,10 +462,16 @@ export const AIChatView = () => {
|
|
|
462
462
|
);
|
|
463
463
|
|
|
464
464
|
const renderUserMessage = React.useCallback(
|
|
465
|
-
async (renderModel: {
|
|
465
|
+
async (renderModel: {
|
|
466
|
+
message: string;
|
|
467
|
+
images?: string[];
|
|
468
|
+
agentId?: string;
|
|
469
|
+
relationId: string;
|
|
470
|
+
command?: string;
|
|
471
|
+
}) => {
|
|
466
472
|
const ChatUserRoleRender = chatRenderRegistry.chatUserRoleRender;
|
|
467
473
|
|
|
468
|
-
const { message, agentId, relationId, command } = renderModel;
|
|
474
|
+
const { message, images, agentId, relationId, command } = renderModel;
|
|
469
475
|
|
|
470
476
|
const visibleAgentId = agentId === ChatProxyService.AGENT_ID ? '' : agentId;
|
|
471
477
|
|
|
@@ -474,12 +480,13 @@ export const AIChatView = () => {
|
|
|
474
480
|
id: uuid(6),
|
|
475
481
|
relationId,
|
|
476
482
|
text: ChatUserRoleRender ? (
|
|
477
|
-
<ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
|
|
483
|
+
<ChatUserRoleRender content={message} images={images} agentId={visibleAgentId} command={command} />
|
|
478
484
|
) : (
|
|
479
485
|
<CodeBlockWrapperInput
|
|
480
486
|
labelService={labelService}
|
|
481
487
|
relationId={relationId}
|
|
482
488
|
text={message}
|
|
489
|
+
images={images}
|
|
483
490
|
agentId={visibleAgentId}
|
|
484
491
|
command={command}
|
|
485
492
|
workspaceService={workspaceService}
|
|
@@ -598,10 +605,10 @@ export const AIChatView = () => {
|
|
|
598
605
|
|
|
599
606
|
const handleAgentReply = React.useCallback(
|
|
600
607
|
async (value: IChatMessageStructure) => {
|
|
601
|
-
const { message, agentId, command, reportExtra } = value;
|
|
608
|
+
const { message, images, agentId, command, reportExtra } = value;
|
|
602
609
|
const { actionType, actionSource } = reportExtra || {};
|
|
603
610
|
|
|
604
|
-
const request = aiChatService.createRequest(message, agentId!, command);
|
|
611
|
+
const request = aiChatService.createRequest(message, agentId!, images, command);
|
|
605
612
|
if (!request) {
|
|
606
613
|
return;
|
|
607
614
|
}
|
|
@@ -627,6 +634,7 @@ export const AIChatView = () => {
|
|
|
627
634
|
|
|
628
635
|
msgHistoryManager.addUserMessage({
|
|
629
636
|
content: message,
|
|
637
|
+
images: images || [],
|
|
630
638
|
agentId: agentId!,
|
|
631
639
|
agentCommand: command!,
|
|
632
640
|
relationId,
|
|
@@ -635,6 +643,7 @@ export const AIChatView = () => {
|
|
|
635
643
|
await renderUserMessage({
|
|
636
644
|
relationId,
|
|
637
645
|
message,
|
|
646
|
+
images,
|
|
638
647
|
command,
|
|
639
648
|
agentId,
|
|
640
649
|
});
|
|
@@ -668,7 +677,7 @@ export const AIChatView = () => {
|
|
|
668
677
|
);
|
|
669
678
|
|
|
670
679
|
const handleSend = React.useCallback(
|
|
671
|
-
async (message: string, agentId?: string, command?: string) => {
|
|
680
|
+
async (message: string, images?: string[], agentId?: string, command?: string) => {
|
|
672
681
|
const reportExtra = {
|
|
673
682
|
actionSource: ActionSourceEnum.Chat,
|
|
674
683
|
actionType: ActionTypeEnum.Send,
|
|
@@ -707,7 +716,7 @@ export const AIChatView = () => {
|
|
|
707
716
|
processedContent = processedContent.replace(match, `\`<attached_folder>${relativePath}\``);
|
|
708
717
|
}
|
|
709
718
|
}
|
|
710
|
-
return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
|
|
719
|
+
return handleAgentReply({ message: processedContent, images, agentId, command, reportExtra });
|
|
711
720
|
},
|
|
712
721
|
[handleAgentReply],
|
|
713
722
|
);
|
|
@@ -750,6 +759,7 @@ export const AIChatView = () => {
|
|
|
750
759
|
message: msg.content,
|
|
751
760
|
agentId: msg.agentId,
|
|
752
761
|
command: msg.agentCommand,
|
|
762
|
+
images: msg.images,
|
|
753
763
|
});
|
|
754
764
|
} else if (msg.role === ChatMessageRole.Assistant && msg.requestId) {
|
|
755
765
|
const request = aiChatService.sessionModel.getRequest(msg.requestId)!;
|
|
@@ -2,6 +2,7 @@ import capitalize from 'lodash/capitalize';
|
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import Highlight from 'react-highlight';
|
|
4
4
|
|
|
5
|
+
import { Image } from '@opensumi/ide-components/lib/image';
|
|
5
6
|
import {
|
|
6
7
|
EDITOR_COMMANDS,
|
|
7
8
|
FILE_COMMANDS,
|
|
@@ -336,6 +337,7 @@ export const CodeBlockWrapper = ({
|
|
|
336
337
|
|
|
337
338
|
export const CodeBlockWrapperInput = ({
|
|
338
339
|
text,
|
|
340
|
+
images,
|
|
339
341
|
relationId,
|
|
340
342
|
agentId,
|
|
341
343
|
command,
|
|
@@ -344,6 +346,7 @@ export const CodeBlockWrapperInput = ({
|
|
|
344
346
|
commandService,
|
|
345
347
|
}: {
|
|
346
348
|
text: string;
|
|
349
|
+
images?: string[];
|
|
347
350
|
relationId: string;
|
|
348
351
|
agentId?: string;
|
|
349
352
|
command?: string;
|
|
@@ -370,6 +373,11 @@ export const CodeBlockWrapperInput = ({
|
|
|
370
373
|
|
|
371
374
|
return (
|
|
372
375
|
<div className={styles.ai_chat_code_wrapper}>
|
|
376
|
+
{images?.map((image) => (
|
|
377
|
+
<div className={styles.image_wrapper}>
|
|
378
|
+
<Image src={image} />
|
|
379
|
+
</div>
|
|
380
|
+
))}
|
|
373
381
|
<div className={styles.render_text}>
|
|
374
382
|
{tag && (
|
|
375
383
|
<div className={styles.tag_wrapper}>
|
|
@@ -162,7 +162,7 @@ const AgentWidget = ({ agentId, command }) => (
|
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
export interface IChatInputProps {
|
|
165
|
-
onSend: (value: string, agentId?: string, command?: string) => void;
|
|
165
|
+
onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
|
|
166
166
|
onValueChange?: (value: string) => void;
|
|
167
167
|
onExpand?: (value: boolean) => void;
|
|
168
168
|
placeholder?: string;
|
|
@@ -341,7 +341,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
const handleSendLogic = (newValue: string = value) => {
|
|
344
|
-
onSend(newValue, agentId, command);
|
|
344
|
+
onSend(newValue, [], agentId, command);
|
|
345
345
|
setValue('');
|
|
346
346
|
setTheme('');
|
|
347
347
|
setAgentId('');
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { DataContent } from 'ai';
|
|
1
2
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
3
|
|
|
4
|
+
import { Image } from '@opensumi/ide-components/lib/image';
|
|
3
5
|
import { LabelService, RecentFilesManager, useInjectable } from '@opensumi/ide-core-browser';
|
|
4
|
-
import { getIcon } from '@opensumi/ide-core-browser/lib/components';
|
|
5
|
-
import { URI, localize } from '@opensumi/ide-core-common';
|
|
6
|
+
import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
|
7
|
+
import { ChatFeatureRegistryToken, URI, localize } from '@opensumi/ide-core-common';
|
|
6
8
|
import { CommandService } from '@opensumi/ide-core-common/lib/command';
|
|
7
9
|
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
|
8
10
|
import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
|
|
11
|
+
import { IMessageService } from '@opensumi/ide-overlay';
|
|
9
12
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
|
10
13
|
|
|
11
14
|
import { IChatInternalService } from '../../common';
|
|
15
|
+
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
|
|
12
16
|
import { ChatInternalService } from '../chat/chat.internal.service';
|
|
13
17
|
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
|
|
14
18
|
|
|
@@ -17,7 +21,13 @@ import { MentionInput } from './mention-input/mention-input';
|
|
|
17
21
|
import { FooterButtonPosition, FooterConfig, MentionItem, MentionType } from './mention-input/types';
|
|
18
22
|
|
|
19
23
|
export interface IChatMentionInputProps {
|
|
20
|
-
onSend: (
|
|
24
|
+
onSend: (
|
|
25
|
+
value: string,
|
|
26
|
+
images?: string[],
|
|
27
|
+
agentId?: string,
|
|
28
|
+
command?: string,
|
|
29
|
+
option?: { model: string; [key: string]: any },
|
|
30
|
+
) => void;
|
|
21
31
|
onValueChange?: (value: string) => void;
|
|
22
32
|
onExpand?: (value: boolean) => void;
|
|
23
33
|
placeholder?: string;
|
|
@@ -26,6 +36,7 @@ export interface IChatMentionInputProps {
|
|
|
26
36
|
sendBtnClassName?: string;
|
|
27
37
|
defaultHeight?: number;
|
|
28
38
|
value?: string;
|
|
39
|
+
images?: Array<DataContent | URL>;
|
|
29
40
|
autoFocus?: boolean;
|
|
30
41
|
theme?: string | null;
|
|
31
42
|
setTheme: (theme: string | null) => void;
|
|
@@ -41,6 +52,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
41
52
|
const { onSend, disabled = false } = props;
|
|
42
53
|
|
|
43
54
|
const [value, setValue] = useState(props.value || '');
|
|
55
|
+
const [images, setImages] = useState(props.images || []);
|
|
44
56
|
const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
|
|
45
57
|
const commandService = useInjectable<CommandService>(CommandService);
|
|
46
58
|
const searchService = useInjectable<IFileSearchService>(FileSearchServicePath);
|
|
@@ -48,7 +60,8 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
48
60
|
const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
|
|
49
61
|
const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
|
50
62
|
const labelService = useInjectable<LabelService>(LabelService);
|
|
51
|
-
|
|
63
|
+
const messageService = useInjectable<IMessageService>(IMessageService);
|
|
64
|
+
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
|
52
65
|
const handleShowMCPConfig = React.useCallback(() => {
|
|
53
66
|
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
|
|
54
67
|
}, [commandService]);
|
|
@@ -239,6 +252,24 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
239
252
|
onClick: handleShowMCPConfig,
|
|
240
253
|
position: FooterButtonPosition.LEFT,
|
|
241
254
|
},
|
|
255
|
+
{
|
|
256
|
+
id: 'upload-image',
|
|
257
|
+
icon: 'image',
|
|
258
|
+
title: localize('aiNative.chat.imageUpload'),
|
|
259
|
+
onClick: () => {
|
|
260
|
+
const input = document.createElement('input');
|
|
261
|
+
input.type = 'file';
|
|
262
|
+
input.accept = 'image/*';
|
|
263
|
+
input.onchange = (e) => {
|
|
264
|
+
const files = (e.target as HTMLInputElement).files;
|
|
265
|
+
if (files?.length) {
|
|
266
|
+
handleImageUpload(Array.from(files));
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
input.click();
|
|
270
|
+
},
|
|
271
|
+
position: FooterButtonPosition.LEFT,
|
|
272
|
+
},
|
|
242
273
|
],
|
|
243
274
|
showModelSelector: true,
|
|
244
275
|
}),
|
|
@@ -254,13 +285,54 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
254
285
|
if (disabled) {
|
|
255
286
|
return;
|
|
256
287
|
}
|
|
257
|
-
onSend(
|
|
288
|
+
onSend(
|
|
289
|
+
content,
|
|
290
|
+
images.map((image) => image.toString()),
|
|
291
|
+
undefined,
|
|
292
|
+
undefined,
|
|
293
|
+
option,
|
|
294
|
+
);
|
|
295
|
+
setImages(props.images || []);
|
|
258
296
|
},
|
|
259
|
-
[onSend,
|
|
297
|
+
[onSend, images, disabled],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const handleImageUpload = useCallback(
|
|
301
|
+
async (files: File[]) => {
|
|
302
|
+
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
|
303
|
+
|
|
304
|
+
// Validate file types
|
|
305
|
+
const invalidFiles = files.filter((file) => !allowedTypes.includes(file.type));
|
|
306
|
+
if (invalidFiles.length > 0) {
|
|
307
|
+
messageService.error('Only JPG, PNG, WebP and GIF images are supported');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const imageUploadProvider = chatFeatureRegistry.getImageUploadProvider();
|
|
312
|
+
if (!imageUploadProvider) {
|
|
313
|
+
messageService.error('No image upload provider found');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Upload all files
|
|
318
|
+
const uploadedData = await Promise.all(files.map((file) => imageUploadProvider.imageUpload(file)));
|
|
319
|
+
|
|
320
|
+
const newImages = [...images, ...uploadedData];
|
|
321
|
+
setImages(newImages);
|
|
322
|
+
},
|
|
323
|
+
[images],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const handleDeleteImage = useCallback(
|
|
327
|
+
(index: number) => {
|
|
328
|
+
setImages(images.filter((_, i) => i !== index));
|
|
329
|
+
},
|
|
330
|
+
[images],
|
|
260
331
|
);
|
|
261
332
|
|
|
262
333
|
return (
|
|
263
334
|
<div className={styles.chat_input_container}>
|
|
335
|
+
{images.length > 0 && <ImagePreviewer images={images} onDelete={handleDeleteImage} />}
|
|
264
336
|
<MentionInput
|
|
265
337
|
mentionItems={defaultMenuItems}
|
|
266
338
|
onSend={handleSend}
|
|
@@ -270,7 +342,29 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
270
342
|
workspaceService={workspaceService}
|
|
271
343
|
placeholder={localize('aiNative.chat.input.placeholder.default')}
|
|
272
344
|
footerConfig={defaultMentionInputFooterOptions}
|
|
345
|
+
onImageUpload={handleImageUpload}
|
|
273
346
|
/>
|
|
274
347
|
</div>
|
|
275
348
|
);
|
|
276
349
|
};
|
|
350
|
+
|
|
351
|
+
const ImagePreviewer = ({
|
|
352
|
+
images,
|
|
353
|
+
onDelete,
|
|
354
|
+
}: {
|
|
355
|
+
images: Array<DataContent | URL>;
|
|
356
|
+
onDelete: (index: number) => void;
|
|
357
|
+
}) => (
|
|
358
|
+
<div>
|
|
359
|
+
<div className={styles.thumbnail_container}>
|
|
360
|
+
{images.map((image, index) => (
|
|
361
|
+
<div key={index} className={styles.thumbnail}>
|
|
362
|
+
<Image src={image.toString()} />
|
|
363
|
+
<button onClick={() => onDelete(index)} className={styles.delete_button}>
|
|
364
|
+
<Icon iconClass='codicon codicon-close' />
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
@@ -601,3 +601,52 @@
|
|
|
601
601
|
vertical-align: middle;
|
|
602
602
|
font-size: 12px;
|
|
603
603
|
}
|
|
604
|
+
|
|
605
|
+
.thumbnail_container {
|
|
606
|
+
display: flex;
|
|
607
|
+
gap: 4px;
|
|
608
|
+
padding: 4px 12px;
|
|
609
|
+
.thumbnail {
|
|
610
|
+
width: 36px;
|
|
611
|
+
height: 36px;
|
|
612
|
+
border-radius: 4px;
|
|
613
|
+
position: relative;
|
|
614
|
+
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
|
|
615
|
+
padding: 2px;
|
|
616
|
+
display: inline-flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
img {
|
|
619
|
+
width: 100%;
|
|
620
|
+
height: 100%;
|
|
621
|
+
object-fit: cover;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
.delete_button {
|
|
625
|
+
position: absolute;
|
|
626
|
+
top: -5px;
|
|
627
|
+
right: -5px;
|
|
628
|
+
font-size: 12px;
|
|
629
|
+
padding: 2px;
|
|
630
|
+
border: 0;
|
|
631
|
+
border-radius: 50%;
|
|
632
|
+
transition: transform 0.2s ease-in-out;
|
|
633
|
+
width: 16px;
|
|
634
|
+
height: 16px;
|
|
635
|
+
display: flex;
|
|
636
|
+
justify-content: center;
|
|
637
|
+
align-items: center;
|
|
638
|
+
background-color: var(--badge-background);
|
|
639
|
+
&:hover {
|
|
640
|
+
cursor: pointer;
|
|
641
|
+
transform: scale(1.2);
|
|
642
|
+
}
|
|
643
|
+
:global(.codicon) {
|
|
644
|
+
font-size: 12px;
|
|
645
|
+
color: var(--badge-foreground);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.image_wrapper {
|
|
651
|
+
display: inline-block;
|
|
652
|
+
}
|
|
@@ -18,6 +18,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
18
18
|
loading = false,
|
|
19
19
|
mentionKeyword = MENTION_KEYWORD,
|
|
20
20
|
onSelectionChange,
|
|
21
|
+
onImageUpload,
|
|
21
22
|
labelService,
|
|
22
23
|
workspaceService,
|
|
23
24
|
placeholder = 'Ask anything, @ to mention',
|
|
@@ -318,14 +319,74 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
318
319
|
inlineSearchStartPos: null,
|
|
319
320
|
loading: false,
|
|
320
321
|
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 处理上下方向键导航历史记录
|
|
325
|
+
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
326
|
+
// 只有在非提及面板激活状态下才处理历史导航
|
|
327
|
+
if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current && history.length > 0) {
|
|
328
|
+
const currentContent = editorRef.current.innerHTML;
|
|
329
|
+
|
|
330
|
+
// 检查是否应该触发历史导航
|
|
331
|
+
const shouldTriggerHistory =
|
|
332
|
+
// 当前内容为空
|
|
333
|
+
!currentContent ||
|
|
334
|
+
currentContent === '<br>' ||
|
|
335
|
+
// 或者当前内容与历史记录中的某一项匹配(正在浏览历史)
|
|
336
|
+
(isNavigatingHistory && historyIndex >= 0 && history[history.length - 1 - historyIndex] === currentContent);
|
|
337
|
+
|
|
338
|
+
if (shouldTriggerHistory) {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
|
|
341
|
+
// 如果是第一次按上下键,保存当前输入
|
|
342
|
+
if (!isNavigatingHistory) {
|
|
343
|
+
setCurrentInput(currentContent);
|
|
344
|
+
setIsNavigatingHistory(true);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 计算新的历史索引
|
|
348
|
+
let newIndex = historyIndex;
|
|
349
|
+
if (e.key === 'ArrowUp') {
|
|
350
|
+
// 向上导航到较早的历史记录
|
|
351
|
+
newIndex = Math.min(history.length - 1, historyIndex + 1);
|
|
352
|
+
} else {
|
|
353
|
+
// 向下导航到较新的历史记录
|
|
354
|
+
newIndex = Math.max(-1, historyIndex - 1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
setHistoryIndex(newIndex);
|
|
358
|
+
|
|
359
|
+
// 更新编辑器内容
|
|
360
|
+
if (newIndex === -1) {
|
|
361
|
+
// 恢复到当前输入
|
|
362
|
+
editorRef.current.innerHTML = currentInput;
|
|
363
|
+
} else {
|
|
364
|
+
// 显示历史记录
|
|
365
|
+
editorRef.current.innerHTML = history[history.length - 1 - newIndex];
|
|
366
|
+
}
|
|
321
367
|
|
|
322
|
-
|
|
368
|
+
// 将光标移到末尾
|
|
369
|
+
const range = document.createRange();
|
|
370
|
+
range.selectNodeContents(editorRef.current);
|
|
371
|
+
range.collapse(false);
|
|
372
|
+
const selection = window.getSelection();
|
|
373
|
+
if (selection) {
|
|
374
|
+
selection.removeAllRanges();
|
|
375
|
+
selection.addRange(range);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
|
|
382
|
+
// 如果用户在浏览历史记录后开始输入其他内容,退出历史导航模式
|
|
383
|
+
setIsNavigatingHistory(false);
|
|
384
|
+
setHistoryIndex(-1);
|
|
323
385
|
}
|
|
324
386
|
|
|
325
387
|
// 添加对 Enter 键的处理,只有在按下 Shift+Enter 时才允许换行
|
|
326
388
|
if (e.key === 'Enter') {
|
|
327
389
|
// 检查是否是输入法的回车键
|
|
328
|
-
// isComposing 属性表示是否正在进行输入法组合输入
|
|
329
390
|
if (e.nativeEvent.isComposing) {
|
|
330
391
|
return; // 如果是输入法组合输入过程中的回车,不做任何处理
|
|
331
392
|
}
|
|
@@ -339,57 +400,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
339
400
|
}
|
|
340
401
|
}
|
|
341
402
|
|
|
342
|
-
// 处理上下方向键导航历史记录
|
|
343
|
-
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
344
|
-
// 只有在非提及面板激活状态下才处理历史导航
|
|
345
|
-
if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
|
346
|
-
e.preventDefault();
|
|
347
|
-
|
|
348
|
-
// 如果是第一次按上下键,保存当前输入
|
|
349
|
-
if (!isNavigatingHistory) {
|
|
350
|
-
setCurrentInput(editorRef.current.innerHTML);
|
|
351
|
-
setIsNavigatingHistory(true);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// 计算新的历史索引
|
|
355
|
-
let newIndex = historyIndex;
|
|
356
|
-
if (e.key === 'ArrowUp') {
|
|
357
|
-
// 向上导航到较早的历史记录
|
|
358
|
-
newIndex = Math.min(history.length - 1, historyIndex + 1);
|
|
359
|
-
} else {
|
|
360
|
-
// 向下导航到较新的历史记录
|
|
361
|
-
newIndex = Math.max(-1, historyIndex - 1);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
setHistoryIndex(newIndex);
|
|
365
|
-
|
|
366
|
-
// 更新编辑器内容
|
|
367
|
-
if (newIndex === -1) {
|
|
368
|
-
// 恢复到当前输入
|
|
369
|
-
editorRef.current.innerHTML = currentInput;
|
|
370
|
-
} else {
|
|
371
|
-
// 显示历史记录
|
|
372
|
-
editorRef.current.innerHTML = history[history.length - 1 - newIndex];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 将光标移到末尾
|
|
376
|
-
const range = document.createRange();
|
|
377
|
-
range.selectNodeContents(editorRef.current);
|
|
378
|
-
range.collapse(false);
|
|
379
|
-
const selection = window.getSelection();
|
|
380
|
-
if (selection) {
|
|
381
|
-
selection.removeAllRanges();
|
|
382
|
-
selection.addRange(range);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
} else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
|
|
388
|
-
// 如果用户在浏览历史记录后开始输入,退出历史导航模式
|
|
389
|
-
setIsNavigatingHistory(false);
|
|
390
|
-
setHistoryIndex(-1);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
403
|
// 如果提及面板未激活,不处理其他键盘事件
|
|
394
404
|
if (!mentionState.active) {
|
|
395
405
|
return;
|
|
@@ -404,8 +414,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
404
414
|
filteredItems = filteredItems.filter((item) => item.text.toLowerCase().includes(searchText));
|
|
405
415
|
}
|
|
406
416
|
|
|
407
|
-
// 二级菜单过滤已经在 getCurrentItems 中处理
|
|
408
|
-
|
|
409
417
|
if (filteredItems.length === 0) {
|
|
410
418
|
return;
|
|
411
419
|
}
|
|
@@ -431,14 +439,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
431
439
|
e.preventDefault();
|
|
432
440
|
}
|
|
433
441
|
}
|
|
434
|
-
|
|
435
|
-
// 处理 Backspace 键,检查是否需要清空编辑器
|
|
436
|
-
if (e.key === 'Backspace' && editorRef.current) {
|
|
437
|
-
const content = editorRef.current.innerHTML;
|
|
438
|
-
if (content === '<br>' || content === '<br/>') {
|
|
439
|
-
editorRef.current.innerHTML = '';
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
442
|
};
|
|
443
443
|
|
|
444
444
|
// 添加对输入法事件的处理
|
|
@@ -447,6 +447,86 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
447
447
|
// 这里可以添加额外的逻辑,如果需要的话
|
|
448
448
|
};
|
|
449
449
|
|
|
450
|
+
// 添加粘贴事件处理
|
|
451
|
+
const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
452
|
+
const items = e.clipboardData.items;
|
|
453
|
+
|
|
454
|
+
// 先收集所有图片文件
|
|
455
|
+
const imageFiles: File[] = [];
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
457
|
+
for (let i = 0; i < items.length; i++) {
|
|
458
|
+
if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
|
|
459
|
+
const file = items[i].getAsFile();
|
|
460
|
+
if (file) {
|
|
461
|
+
imageFiles.push(file);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
|
|
468
|
+
// 处理所有收集到的图片
|
|
469
|
+
if (imageFiles.length > 0 && onImageUpload) {
|
|
470
|
+
await onImageUpload(imageFiles);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const text = e.clipboardData.getData('text/plain');
|
|
475
|
+
|
|
476
|
+
// 处理文本,保留换行和缩进
|
|
477
|
+
const processedText = text
|
|
478
|
+
.replace(/\t/g, ' ')
|
|
479
|
+
.replace(/\n\s*\n/g, '\n\n')
|
|
480
|
+
.replace(/[ \t]+$/gm, '');
|
|
481
|
+
|
|
482
|
+
const selection = window.getSelection();
|
|
483
|
+
if (!selection || !selection.rangeCount) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const range = selection.getRangeAt(0);
|
|
488
|
+
range.deleteContents();
|
|
489
|
+
|
|
490
|
+
// 将处理后的文本按行分割
|
|
491
|
+
const lines = processedText.split('\n');
|
|
492
|
+
const fragment = document.createDocumentFragment();
|
|
493
|
+
|
|
494
|
+
lines.forEach((line, index) => {
|
|
495
|
+
// 处理行首空格,将每个空格转换为
|
|
496
|
+
const processedLine = line.replace(/^[ ]+/g, (match) => {
|
|
497
|
+
const span = document.createElement('span');
|
|
498
|
+
span.innerHTML = '\u00A0'.repeat(match.length);
|
|
499
|
+
return span.innerHTML;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// 创建一个临时容器来保持 HTML 内容
|
|
503
|
+
const container = document.createElement('span');
|
|
504
|
+
container.innerHTML = processedLine;
|
|
505
|
+
|
|
506
|
+
// 将容器的内容添加到文档片段
|
|
507
|
+
while (container.firstChild) {
|
|
508
|
+
fragment.appendChild(container.firstChild);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 如果不是最后一行,添加换行符
|
|
512
|
+
if (index < lines.length - 1) {
|
|
513
|
+
fragment.appendChild(document.createElement('br'));
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// 插入处理后的内容
|
|
518
|
+
range.insertNode(fragment);
|
|
519
|
+
|
|
520
|
+
// 将光标移动到插入内容的末尾
|
|
521
|
+
range.setStartAfter(fragment);
|
|
522
|
+
range.setEndAfter(fragment);
|
|
523
|
+
selection.removeAllRanges();
|
|
524
|
+
selection.addRange(range);
|
|
525
|
+
|
|
526
|
+
// 触发 input 事件以更新状态
|
|
527
|
+
handleInput();
|
|
528
|
+
};
|
|
529
|
+
|
|
450
530
|
// 初始化编辑器
|
|
451
531
|
React.useEffect(() => {
|
|
452
532
|
if (editorRef.current) {
|
|
@@ -867,7 +947,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
867
947
|
title={button.title}
|
|
868
948
|
>
|
|
869
949
|
<EnhanceIcon
|
|
870
|
-
className={cls(getIcon(button.icon), styles[`${button.id}_logo`])}
|
|
950
|
+
className={cls(button.icon ? getIcon(button.icon) : button.iconClass, styles[`${button.id}_logo`])}
|
|
871
951
|
tabIndex={0}
|
|
872
952
|
role='button'
|
|
873
953
|
ariaLabel={button.title}
|
|
@@ -901,6 +981,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
901
981
|
contentEditable={true}
|
|
902
982
|
onInput={handleInput}
|
|
903
983
|
onKeyDown={handleKeyDown}
|
|
984
|
+
onPaste={handlePaste}
|
|
904
985
|
onCompositionEnd={handleCompositionEnd}
|
|
905
986
|
/>
|
|
906
987
|
</div>
|
|
@@ -55,7 +55,8 @@ export enum MentionType {
|
|
|
55
55
|
|
|
56
56
|
interface FooterButton {
|
|
57
57
|
id: string;
|
|
58
|
-
icon
|
|
58
|
+
icon?: string;
|
|
59
|
+
iconClass?: string;
|
|
59
60
|
title: string;
|
|
60
61
|
onClick?: () => void;
|
|
61
62
|
position: FooterButtonPosition;
|
|
@@ -75,6 +76,7 @@ export interface MentionInputProps {
|
|
|
75
76
|
placeholder?: string;
|
|
76
77
|
loading?: boolean;
|
|
77
78
|
onSelectionChange?: (value: string) => void;
|
|
79
|
+
onImageUpload?: (files: File[]) => Promise<void>;
|
|
78
80
|
footerConfig?: FooterConfig; // 新增配置项
|
|
79
81
|
mentionKeyword?: string;
|
|
80
82
|
labelService?: LabelService;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DataContent } from 'ai';
|
|
2
|
+
|
|
1
3
|
import { Autowired, Injectable } from '@opensumi/di';
|
|
2
4
|
import { AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider';
|
|
3
5
|
import { WithEventBus } from '@opensumi/ide-core-common/lib/event-bus/event-decorator';
|