@opensumi/ide-ai-native 3.8.3-next-1741945847.0 → 3.8.3-next-1741949132.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 +51 -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 +74 -112
- package/lib/browser/components/mention-input/mention-input.js.map +1 -1
- package/lib/browser/components/mention-input/mention-input.module.less +1 -0
- 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/config/components/mcp-config.view.d.ts.map +1 -1
- package/lib/browser/mcp/config/components/mcp-config.view.js +1 -15
- package/lib/browser/mcp/config/components/mcp-config.view.js.map +1 -1
- package/lib/browser/mcp/mcp-server-proxy.service.d.ts +0 -1
- package/lib/browser/mcp/mcp-server-proxy.service.d.ts.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 -1
- 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 +93 -6
- package/src/browser/components/components.module.less +49 -0
- package/src/browser/components/mention-input/mention-input.module.less +1 -0
- package/src/browser/components/mention-input/mention-input.tsx +81 -129
- package/src/browser/components/mention-input/types.ts +3 -1
- package/src/browser/context/llm-context.service.ts +2 -0
- package/src/browser/mcp/config/components/mcp-config.view.tsx +1 -15
- package/src/browser/model/msg-history-manager.ts +1 -1
- package/src/browser/types.ts +8 -1
- package/src/common/index.ts +7 -8
- package/src/common/llm-context.ts +2 -0
- package/src/node/base-language-model.ts +25 -3
|
@@ -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
|
+
iconClass: 'codicon codicon-file-media',
|
|
258
|
+
title: 'Upload Image',
|
|
259
|
+
onClick: () => {
|
|
260
|
+
const input = document.createElement('input');
|
|
261
|
+
input.type = 'file';
|
|
262
|
+
input.accept = 'image/*';
|
|
263
|
+
input.onchange = (e) => {
|
|
264
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
265
|
+
if (file) {
|
|
266
|
+
handleImageUpload(file);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
input.click();
|
|
270
|
+
},
|
|
271
|
+
position: FooterButtonPosition.LEFT,
|
|
272
|
+
},
|
|
242
273
|
],
|
|
243
274
|
showModelSelector: true,
|
|
244
275
|
}),
|
|
@@ -254,13 +285,47 @@ 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 || []);
|
|
296
|
+
},
|
|
297
|
+
[onSend, images, disabled],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const handleImageUpload = useCallback(
|
|
301
|
+
async (file: File) => {
|
|
302
|
+
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
|
303
|
+
if (!allowedTypes.includes(file.type)) {
|
|
304
|
+
messageService.error('Only JPG, PNG, WebP and GIF images are supported');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const imageUploadProvider = chatFeatureRegistry.getImageUploadProvider();
|
|
309
|
+
if (!imageUploadProvider) {
|
|
310
|
+
messageService.error('No image upload provider found');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const data = await imageUploadProvider.imageUpload(file);
|
|
314
|
+
setImages([...images, data]);
|
|
258
315
|
},
|
|
259
|
-
[
|
|
316
|
+
[images],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const handleDeleteImage = useCallback(
|
|
320
|
+
(index: number) => {
|
|
321
|
+
setImages(images.filter((_, i) => i !== index));
|
|
322
|
+
},
|
|
323
|
+
[images],
|
|
260
324
|
);
|
|
261
325
|
|
|
262
326
|
return (
|
|
263
327
|
<div className={styles.chat_input_container}>
|
|
328
|
+
{images.length > 0 && <ImagePreviewer images={images} onDelete={handleDeleteImage} />}
|
|
264
329
|
<MentionInput
|
|
265
330
|
mentionItems={defaultMenuItems}
|
|
266
331
|
onSend={handleSend}
|
|
@@ -270,7 +335,29 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
270
335
|
workspaceService={workspaceService}
|
|
271
336
|
placeholder={localize('aiNative.chat.input.placeholder.default')}
|
|
272
337
|
footerConfig={defaultMentionInputFooterOptions}
|
|
338
|
+
onImageUpload={handleImageUpload}
|
|
273
339
|
/>
|
|
274
340
|
</div>
|
|
275
341
|
);
|
|
276
342
|
};
|
|
343
|
+
|
|
344
|
+
const ImagePreviewer = ({
|
|
345
|
+
images,
|
|
346
|
+
onDelete,
|
|
347
|
+
}: {
|
|
348
|
+
images: Array<DataContent | URL>;
|
|
349
|
+
onDelete: (index: number) => void;
|
|
350
|
+
}) => (
|
|
351
|
+
<div>
|
|
352
|
+
<div className={styles.thumbnail_container}>
|
|
353
|
+
{images.map((image, index) => (
|
|
354
|
+
<div key={index} className={styles.thumbnail}>
|
|
355
|
+
<Image src={image.toString()} />
|
|
356
|
+
<button onClick={() => onDelete(index)} className={styles.delete_button}>
|
|
357
|
+
<Icon iconClass='codicon codicon-close' />
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
@@ -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',
|
|
@@ -275,6 +276,21 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
275
276
|
}
|
|
276
277
|
};
|
|
277
278
|
|
|
279
|
+
// 处理图片粘贴事件
|
|
280
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
281
|
+
const items = e.clipboardData.items;
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
283
|
+
for (let i = 0; i < items.length; i++) {
|
|
284
|
+
if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
|
|
285
|
+
const file = items[i].getAsFile();
|
|
286
|
+
if (file && onImageUpload) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
onImageUpload(file);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
278
294
|
// 处理键盘事件
|
|
279
295
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
280
296
|
// 如果按下ESC键且提及面板处于活动状态或内联搜索处于活动状态
|
|
@@ -318,74 +334,14 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
318
334
|
inlineSearchStartPos: null,
|
|
319
335
|
loading: false,
|
|
320
336
|
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// 处理上下方向键导航历史记录
|
|
324
|
-
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
325
|
-
// 只有在非提及面板激活状态下才处理历史导航
|
|
326
|
-
if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current && history.length > 0) {
|
|
327
|
-
const currentContent = editorRef.current.innerHTML;
|
|
328
|
-
|
|
329
|
-
// 检查是否应该触发历史导航
|
|
330
|
-
const shouldTriggerHistory =
|
|
331
|
-
// 当前内容为空
|
|
332
|
-
!currentContent ||
|
|
333
|
-
currentContent === '<br>' ||
|
|
334
|
-
// 或者当前内容与历史记录中的某一项匹配(正在浏览历史)
|
|
335
|
-
(isNavigatingHistory && historyIndex >= 0 && history[history.length - 1 - historyIndex] === currentContent);
|
|
336
|
-
|
|
337
|
-
if (shouldTriggerHistory) {
|
|
338
|
-
e.preventDefault();
|
|
339
|
-
|
|
340
|
-
// 如果是第一次按上下键,保存当前输入
|
|
341
|
-
if (!isNavigatingHistory) {
|
|
342
|
-
setCurrentInput(currentContent);
|
|
343
|
-
setIsNavigatingHistory(true);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// 计算新的历史索引
|
|
347
|
-
let newIndex = historyIndex;
|
|
348
|
-
if (e.key === 'ArrowUp') {
|
|
349
|
-
// 向上导航到较早的历史记录
|
|
350
|
-
newIndex = Math.min(history.length - 1, historyIndex + 1);
|
|
351
|
-
} else {
|
|
352
|
-
// 向下导航到较新的历史记录
|
|
353
|
-
newIndex = Math.max(-1, historyIndex - 1);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
setHistoryIndex(newIndex);
|
|
357
337
|
|
|
358
|
-
|
|
359
|
-
if (newIndex === -1) {
|
|
360
|
-
// 恢复到当前输入
|
|
361
|
-
editorRef.current.innerHTML = currentInput;
|
|
362
|
-
} else {
|
|
363
|
-
// 显示历史记录
|
|
364
|
-
editorRef.current.innerHTML = history[history.length - 1 - newIndex];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 将光标移到末尾
|
|
368
|
-
const range = document.createRange();
|
|
369
|
-
range.selectNodeContents(editorRef.current);
|
|
370
|
-
range.collapse(false);
|
|
371
|
-
const selection = window.getSelection();
|
|
372
|
-
if (selection) {
|
|
373
|
-
selection.removeAllRanges();
|
|
374
|
-
selection.addRange(range);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
} else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
|
|
381
|
-
// 如果用户在浏览历史记录后开始输入其他内容,退出历史导航模式
|
|
382
|
-
setIsNavigatingHistory(false);
|
|
383
|
-
setHistoryIndex(-1);
|
|
338
|
+
// 不要阻止默认行为,让 @ 正常输入到编辑器中
|
|
384
339
|
}
|
|
385
340
|
|
|
386
341
|
// 添加对 Enter 键的处理,只有在按下 Shift+Enter 时才允许换行
|
|
387
342
|
if (e.key === 'Enter') {
|
|
388
343
|
// 检查是否是输入法的回车键
|
|
344
|
+
// isComposing 属性表示是否正在进行输入法组合输入
|
|
389
345
|
if (e.nativeEvent.isComposing) {
|
|
390
346
|
return; // 如果是输入法组合输入过程中的回车,不做任何处理
|
|
391
347
|
}
|
|
@@ -399,6 +355,57 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
399
355
|
}
|
|
400
356
|
}
|
|
401
357
|
|
|
358
|
+
// 处理上下方向键导航历史记录
|
|
359
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
360
|
+
// 只有在非提及面板激活状态下才处理历史导航
|
|
361
|
+
if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
|
|
364
|
+
// 如果是第一次按上下键,保存当前输入
|
|
365
|
+
if (!isNavigatingHistory) {
|
|
366
|
+
setCurrentInput(editorRef.current.innerHTML);
|
|
367
|
+
setIsNavigatingHistory(true);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 计算新的历史索引
|
|
371
|
+
let newIndex = historyIndex;
|
|
372
|
+
if (e.key === 'ArrowUp') {
|
|
373
|
+
// 向上导航到较早的历史记录
|
|
374
|
+
newIndex = Math.min(history.length - 1, historyIndex + 1);
|
|
375
|
+
} else {
|
|
376
|
+
// 向下导航到较新的历史记录
|
|
377
|
+
newIndex = Math.max(-1, historyIndex - 1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
setHistoryIndex(newIndex);
|
|
381
|
+
|
|
382
|
+
// 更新编辑器内容
|
|
383
|
+
if (newIndex === -1) {
|
|
384
|
+
// 恢复到当前输入
|
|
385
|
+
editorRef.current.innerHTML = currentInput;
|
|
386
|
+
} else {
|
|
387
|
+
// 显示历史记录
|
|
388
|
+
editorRef.current.innerHTML = history[history.length - 1 - newIndex];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 将光标移到末尾
|
|
392
|
+
const range = document.createRange();
|
|
393
|
+
range.selectNodeContents(editorRef.current);
|
|
394
|
+
range.collapse(false);
|
|
395
|
+
const selection = window.getSelection();
|
|
396
|
+
if (selection) {
|
|
397
|
+
selection.removeAllRanges();
|
|
398
|
+
selection.addRange(range);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
} else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
|
|
404
|
+
// 如果用户在浏览历史记录后开始输入,退出历史导航模式
|
|
405
|
+
setIsNavigatingHistory(false);
|
|
406
|
+
setHistoryIndex(-1);
|
|
407
|
+
}
|
|
408
|
+
|
|
402
409
|
// 如果提及面板未激活,不处理其他键盘事件
|
|
403
410
|
if (!mentionState.active) {
|
|
404
411
|
return;
|
|
@@ -413,6 +420,8 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
413
420
|
filteredItems = filteredItems.filter((item) => item.text.toLowerCase().includes(searchText));
|
|
414
421
|
}
|
|
415
422
|
|
|
423
|
+
// 二级菜单过滤已经在 getCurrentItems 中处理
|
|
424
|
+
|
|
416
425
|
if (filteredItems.length === 0) {
|
|
417
426
|
return;
|
|
418
427
|
}
|
|
@@ -438,6 +447,14 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
438
447
|
e.preventDefault();
|
|
439
448
|
}
|
|
440
449
|
}
|
|
450
|
+
|
|
451
|
+
// 处理 Backspace 键,检查是否需要清空编辑器
|
|
452
|
+
if (e.key === 'Backspace' && editorRef.current) {
|
|
453
|
+
const content = editorRef.current.innerHTML;
|
|
454
|
+
if (content === '<br>' || content === '<br/>') {
|
|
455
|
+
editorRef.current.innerHTML = '';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
441
458
|
};
|
|
442
459
|
|
|
443
460
|
// 添加对输入法事件的处理
|
|
@@ -446,71 +463,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
446
463
|
// 这里可以添加额外的逻辑,如果需要的话
|
|
447
464
|
};
|
|
448
465
|
|
|
449
|
-
// 添加粘贴事件处理
|
|
450
|
-
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
451
|
-
// 阻止默认粘贴行为
|
|
452
|
-
e.preventDefault();
|
|
453
|
-
|
|
454
|
-
// 获取剪贴板中的纯文本内容
|
|
455
|
-
const text = e.clipboardData.getData('text/plain');
|
|
456
|
-
|
|
457
|
-
// 处理文本,保留换行和缩进
|
|
458
|
-
const processedText = text
|
|
459
|
-
// 将制表符转换为4个空格
|
|
460
|
-
.replace(/\t/g, ' ')
|
|
461
|
-
// 将连续的换行符转换为单个换行
|
|
462
|
-
.replace(/\n\s*\n/g, '\n\n')
|
|
463
|
-
// 移除行尾空格
|
|
464
|
-
.replace(/[ \t]+$/gm, '');
|
|
465
|
-
|
|
466
|
-
const selection = window.getSelection();
|
|
467
|
-
if (!selection || !selection.rangeCount) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const range = selection.getRangeAt(0);
|
|
472
|
-
range.deleteContents();
|
|
473
|
-
|
|
474
|
-
// 将处理后的文本按行分割
|
|
475
|
-
const lines = processedText.split('\n');
|
|
476
|
-
const fragment = document.createDocumentFragment();
|
|
477
|
-
|
|
478
|
-
lines.forEach((line, index) => {
|
|
479
|
-
// 处理行首空格,将每个空格转换为
|
|
480
|
-
const processedLine = line.replace(/^[ ]+/g, (match) => {
|
|
481
|
-
const span = document.createElement('span');
|
|
482
|
-
span.innerHTML = '\u00A0'.repeat(match.length);
|
|
483
|
-
return span.innerHTML;
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// 创建一个临时容器来保持 HTML 内容
|
|
487
|
-
const container = document.createElement('span');
|
|
488
|
-
container.innerHTML = processedLine;
|
|
489
|
-
|
|
490
|
-
// 将容器的内容添加到文档片段
|
|
491
|
-
while (container.firstChild) {
|
|
492
|
-
fragment.appendChild(container.firstChild);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// 如果不是最后一行,添加换行符
|
|
496
|
-
if (index < lines.length - 1) {
|
|
497
|
-
fragment.appendChild(document.createElement('br'));
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
// 插入处理后的内容
|
|
502
|
-
range.insertNode(fragment);
|
|
503
|
-
|
|
504
|
-
// 将光标移动到插入内容的末尾
|
|
505
|
-
range.setStartAfter(fragment);
|
|
506
|
-
range.setEndAfter(fragment);
|
|
507
|
-
selection.removeAllRanges();
|
|
508
|
-
selection.addRange(range);
|
|
509
|
-
|
|
510
|
-
// 触发 input 事件以更新状态
|
|
511
|
-
handleInput();
|
|
512
|
-
};
|
|
513
|
-
|
|
514
466
|
// 初始化编辑器
|
|
515
467
|
React.useEffect(() => {
|
|
516
468
|
if (editorRef.current) {
|
|
@@ -931,7 +883,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
931
883
|
title={button.title}
|
|
932
884
|
>
|
|
933
885
|
<EnhanceIcon
|
|
934
|
-
className={cls(getIcon(button.icon), styles[`${button.id}_logo`])}
|
|
886
|
+
className={cls(button.icon ? getIcon(button.icon) : button.iconClass, styles[`${button.id}_logo`])}
|
|
935
887
|
tabIndex={0}
|
|
936
888
|
role='button'
|
|
937
889
|
ariaLabel={button.title}
|
|
@@ -965,8 +917,8 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
965
917
|
contentEditable={true}
|
|
966
918
|
onInput={handleInput}
|
|
967
919
|
onKeyDown={handleKeyDown}
|
|
968
|
-
onCompositionEnd={handleCompositionEnd}
|
|
969
920
|
onPaste={handlePaste}
|
|
921
|
+
onCompositionEnd={handleCompositionEnd}
|
|
970
922
|
/>
|
|
971
923
|
</div>
|
|
972
924
|
<div className={styles.footer}>
|
|
@@ -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?: (file: File) => 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';
|
|
@@ -26,21 +26,7 @@ export const MCPConfigView: React.FC = () => {
|
|
|
26
26
|
const [editingServer, setEditingServer] = React.useState<MCPServerFormData | undefined>();
|
|
27
27
|
const [loadingServer, setLoadingServer] = React.useState<string | undefined>();
|
|
28
28
|
const loadServers = useCallback(async () => {
|
|
29
|
-
const
|
|
30
|
-
const runningServers = await mcpServerProxyService.$getServers();
|
|
31
|
-
const builtinServer = runningServers.find((server) => server.name === BUILTIN_MCP_SERVER_NAME);
|
|
32
|
-
const allServers = userServers.map((server) => {
|
|
33
|
-
const runningServer = runningServers.find((s) => s.name === server.name);
|
|
34
|
-
return {
|
|
35
|
-
...server,
|
|
36
|
-
name: server.name,
|
|
37
|
-
isStarted: !!runningServer,
|
|
38
|
-
tools: runningServer?.tools,
|
|
39
|
-
};
|
|
40
|
-
}) as MCPServer[];
|
|
41
|
-
if (builtinServer) {
|
|
42
|
-
allServers.unshift(builtinServer);
|
|
43
|
-
}
|
|
29
|
+
const allServers = await mcpServerProxyService.$getServers();
|
|
44
30
|
setServers(allServers);
|
|
45
31
|
}, [mcpServerProxyService]);
|
|
46
32
|
|
|
@@ -62,7 +62,7 @@ export class MsgHistoryManager extends Disposable {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
public addUserMessage(
|
|
65
|
-
message: Required<Pick<IExcludeMessage, 'agentId' | 'agentCommand' | 'content' | 'relationId'>>,
|
|
65
|
+
message: Required<Pick<IExcludeMessage, 'agentId' | 'agentCommand' | 'content' | 'relationId' | 'images'>>,
|
|
66
66
|
): string {
|
|
67
67
|
return this.doAddMessage({
|
|
68
68
|
...message,
|
package/src/browser/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DataContent } from 'ai';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { ZodSchema } from 'zod';
|
|
3
4
|
|
|
@@ -129,6 +130,7 @@ export interface IChatSlashCommandHandler {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
export interface IChatFeatureRegistry {
|
|
133
|
+
registerImageUploadProvider(provider: IImageUploadProvider): void;
|
|
132
134
|
registerWelcome(content: IChatWelcomeMessageContent | React.ReactNode, sampleQuestions?: ISampleQuestions[]): void;
|
|
133
135
|
registerSlashCommand(command: IChatSlashCommandItem, handler: IChatSlashCommandHandler): void;
|
|
134
136
|
}
|
|
@@ -140,13 +142,14 @@ export type ChatWelcomeRender = (props: {
|
|
|
140
142
|
export type ChatAIRoleRender = (props: { content: string }) => React.ReactElement | React.JSX.Element;
|
|
141
143
|
export type ChatUserRoleRender = (props: {
|
|
142
144
|
content: string;
|
|
145
|
+
images?: string[];
|
|
143
146
|
agentId?: string;
|
|
144
147
|
command?: string;
|
|
145
148
|
}) => React.ReactElement | React.JSX.Element;
|
|
146
149
|
export type ChatThinkingRender = (props: { thinkingText?: string }) => React.ReactElement | React.JSX.Element;
|
|
147
150
|
export type ChatThinkingResultRender = (props: { thinkingResult?: string }) => React.ReactElement | React.JSX.Element;
|
|
148
151
|
export type ChatInputRender = (props: {
|
|
149
|
-
onSend: (value: string, agentId?: string, command?: string) => void;
|
|
152
|
+
onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
|
|
150
153
|
onValueChange?: (value: string) => void;
|
|
151
154
|
onExpand?: (value: boolean) => void;
|
|
152
155
|
placeholder?: string;
|
|
@@ -289,6 +292,10 @@ export interface IProblemFixProviderRegistry {
|
|
|
289
292
|
registerHoverFixProvider(handler: IHoverFixHandler): void;
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
export interface IImageUploadProvider {
|
|
296
|
+
imageUpload(file: File): Promise<DataContent | URL>;
|
|
297
|
+
}
|
|
298
|
+
|
|
292
299
|
export const AINativeCoreContribution = Symbol('AINativeCoreContribution');
|
|
293
300
|
|
|
294
301
|
export interface AINativeCoreContribution {
|
package/src/common/index.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface IChatMessageStructure {
|
|
|
52
52
|
* 用于 chat 面板展示
|
|
53
53
|
*/
|
|
54
54
|
message: string;
|
|
55
|
+
/**
|
|
56
|
+
* 图片
|
|
57
|
+
*/
|
|
58
|
+
images?: string[];
|
|
55
59
|
/**
|
|
56
60
|
* 实际调用的 prompt
|
|
57
61
|
*/
|
|
@@ -134,14 +138,7 @@ export interface ISumiMCPServerBackend {
|
|
|
134
138
|
initBuiltinMCPServer(enabled: boolean): void;
|
|
135
139
|
initExternalMCPServers(servers: MCPServerDescription[]): void;
|
|
136
140
|
getAllMCPTools(): Promise<MCPTool[]>;
|
|
137
|
-
getServers(): Promise<
|
|
138
|
-
Array<{
|
|
139
|
-
name: string;
|
|
140
|
-
isStarted: boolean;
|
|
141
|
-
type: string;
|
|
142
|
-
tools: MCPTool[];
|
|
143
|
-
}>
|
|
144
|
-
>;
|
|
141
|
+
getServers(): Promise<Array<{ name: string; isStarted: boolean; tools: MCPTool[] }>>;
|
|
145
142
|
startServer(serverName: string): Promise<void>;
|
|
146
143
|
stopServer(serverName: string): Promise<void>;
|
|
147
144
|
addOrUpdateServer(description: MCPServerDescription): void;
|
|
@@ -207,6 +204,7 @@ export interface IChatAgentRequest {
|
|
|
207
204
|
requestId: string;
|
|
208
205
|
command?: string;
|
|
209
206
|
message: string;
|
|
207
|
+
images?: string[];
|
|
210
208
|
regenerate?: boolean;
|
|
211
209
|
}
|
|
212
210
|
|
|
@@ -244,6 +242,7 @@ export type IChatFollowup = IChatReplyFollowup | IChatResponseCommandFollowup;
|
|
|
244
242
|
|
|
245
243
|
export interface IChatRequestMessage {
|
|
246
244
|
prompt: string;
|
|
245
|
+
images?: string[];
|
|
247
246
|
agentId: string;
|
|
248
247
|
command?: string;
|
|
249
248
|
}
|