@notebook-intelligence/notebook-intelligence 1.2.2
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/LICENSE +674 -0
- package/README.md +151 -0
- package/lib/api.d.ts +47 -0
- package/lib/api.js +246 -0
- package/lib/chat-sidebar.d.ts +80 -0
- package/lib/chat-sidebar.js +1277 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +1078 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +60 -0
- package/lib/tokens.d.ts +93 -0
- package/lib/tokens.js +51 -0
- package/lib/utils.d.ts +17 -0
- package/lib/utils.js +163 -0
- package/package.json +219 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +333 -0
- package/src/chat-sidebar.tsx +2171 -0
- package/src/handler.ts +51 -0
- package/src/index.ts +1398 -0
- package/src/markdown-renderer.tsx +140 -0
- package/src/svg.d.ts +4 -0
- package/src/tokens.ts +114 -0
- package/src/utils.ts +204 -0
- package/style/base.css +590 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,2171 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
ChangeEvent,
|
|
5
|
+
KeyboardEvent,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { ReactWidget } from '@jupyterlab/apputils';
|
|
13
|
+
import { UUID } from '@lumino/coreutils';
|
|
14
|
+
|
|
15
|
+
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
|
|
16
|
+
|
|
17
|
+
import { NBIAPI, GitHubCopilotLoginStatus } from './api';
|
|
18
|
+
import {
|
|
19
|
+
BackendMessageType,
|
|
20
|
+
ContextType,
|
|
21
|
+
GITHUB_COPILOT_PROVIDER_ID,
|
|
22
|
+
IActiveDocumentInfo,
|
|
23
|
+
ICellContents,
|
|
24
|
+
IChatCompletionResponseEmitter,
|
|
25
|
+
IChatParticipant,
|
|
26
|
+
IContextItem,
|
|
27
|
+
ITelemetryEmitter,
|
|
28
|
+
RequestDataType,
|
|
29
|
+
ResponseStreamDataType,
|
|
30
|
+
TelemetryEventType
|
|
31
|
+
} from './tokens';
|
|
32
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
33
|
+
import { MarkdownRenderer } from './markdown-renderer';
|
|
34
|
+
|
|
35
|
+
import copySvgstr from '../style/icons/copy.svg';
|
|
36
|
+
import copilotSvgstr from '../style/icons/copilot.svg';
|
|
37
|
+
import copilotWarningSvgstr from '../style/icons/copilot-warning.svg';
|
|
38
|
+
import {
|
|
39
|
+
VscSend,
|
|
40
|
+
VscStopCircle,
|
|
41
|
+
VscEye,
|
|
42
|
+
VscEyeClosed,
|
|
43
|
+
VscTriangleRight,
|
|
44
|
+
VscTriangleDown
|
|
45
|
+
} from 'react-icons/vsc';
|
|
46
|
+
import { extractLLMGeneratedCode, isDarkTheme } from './utils';
|
|
47
|
+
|
|
48
|
+
const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
|
|
49
|
+
const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
|
|
50
|
+
const OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
|
|
51
|
+
'openai-compatible-inline-completion-model';
|
|
52
|
+
const LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
|
|
53
|
+
'litellm-compatible-inline-completion-model';
|
|
54
|
+
|
|
55
|
+
export enum RunChatCompletionType {
|
|
56
|
+
Chat,
|
|
57
|
+
ExplainThis,
|
|
58
|
+
FixThis,
|
|
59
|
+
GenerateCode,
|
|
60
|
+
ExplainThisOutput,
|
|
61
|
+
TroubleshootThisOutput
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface IRunChatCompletionRequest {
|
|
65
|
+
messageId: string;
|
|
66
|
+
chatId: string;
|
|
67
|
+
type: RunChatCompletionType;
|
|
68
|
+
content: string;
|
|
69
|
+
language?: string;
|
|
70
|
+
filename?: string;
|
|
71
|
+
prefix?: string;
|
|
72
|
+
suffix?: string;
|
|
73
|
+
existingCode?: string;
|
|
74
|
+
additionalContext?: IContextItem[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IChatSidebarOptions {
|
|
78
|
+
getActiveDocumentInfo: () => IActiveDocumentInfo;
|
|
79
|
+
getActiveSelectionContent: () => string;
|
|
80
|
+
getCurrentCellContents: () => ICellContents;
|
|
81
|
+
openFile: (path: string) => void;
|
|
82
|
+
getApp: () => JupyterFrontEnd;
|
|
83
|
+
getTelemetryEmitter: () => ITelemetryEmitter;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class ChatSidebar extends ReactWidget {
|
|
87
|
+
constructor(options: IChatSidebarOptions) {
|
|
88
|
+
super();
|
|
89
|
+
|
|
90
|
+
this._options = options;
|
|
91
|
+
this.node.style.height = '100%';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
render(): JSX.Element {
|
|
95
|
+
return (
|
|
96
|
+
<SidebarComponent
|
|
97
|
+
getActiveDocumentInfo={this._options.getActiveDocumentInfo}
|
|
98
|
+
getActiveSelectionContent={this._options.getActiveSelectionContent}
|
|
99
|
+
getCurrentCellContents={this._options.getCurrentCellContents}
|
|
100
|
+
openFile={this._options.openFile}
|
|
101
|
+
getApp={this._options.getApp}
|
|
102
|
+
getTelemetryEmitter={this._options.getTelemetryEmitter}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private _options: IChatSidebarOptions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface IInlinePromptWidgetOptions {
|
|
111
|
+
prompt: string;
|
|
112
|
+
existingCode: string;
|
|
113
|
+
prefix: string;
|
|
114
|
+
suffix: string;
|
|
115
|
+
onRequestSubmitted: (prompt: string) => void;
|
|
116
|
+
onRequestCancelled: () => void;
|
|
117
|
+
onContentStream: (content: string) => void;
|
|
118
|
+
onContentStreamEnd: () => void;
|
|
119
|
+
onUpdatedCodeChange: (content: string) => void;
|
|
120
|
+
onUpdatedCodeAccepted: () => void;
|
|
121
|
+
telemetryEmitter: ITelemetryEmitter;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class InlinePromptWidget extends ReactWidget {
|
|
125
|
+
constructor(rect: DOMRect, options: IInlinePromptWidgetOptions) {
|
|
126
|
+
super();
|
|
127
|
+
|
|
128
|
+
this.node.classList.add('inline-prompt-widget');
|
|
129
|
+
this.node.style.top = `${rect.top + 32}px`;
|
|
130
|
+
this.node.style.left = `${rect.left}px`;
|
|
131
|
+
this.node.style.width = rect.width + 'px';
|
|
132
|
+
this.node.style.height = '48px';
|
|
133
|
+
this._options = options;
|
|
134
|
+
|
|
135
|
+
this.node.addEventListener('focusout', (event: any) => {
|
|
136
|
+
if (this.node.contains(event.relatedTarget)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this._options.onRequestCancelled();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
updatePosition(rect: DOMRect) {
|
|
145
|
+
this.node.style.top = `${rect.top + 32}px`;
|
|
146
|
+
this.node.style.left = `${rect.left}px`;
|
|
147
|
+
this.node.style.width = rect.width + 'px';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_onResponse(response: any) {
|
|
151
|
+
if (response.type === BackendMessageType.StreamMessage) {
|
|
152
|
+
const delta = response.data['choices']?.[0]?.['delta'];
|
|
153
|
+
if (!delta) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const responseMessage =
|
|
157
|
+
response.data['choices']?.[0]?.['delta']?.['content'];
|
|
158
|
+
if (!responseMessage) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this._options.onContentStream(responseMessage);
|
|
162
|
+
} else if (response.type === BackendMessageType.StreamEnd) {
|
|
163
|
+
this._options.onContentStreamEnd();
|
|
164
|
+
const timeElapsed =
|
|
165
|
+
(new Date().getTime() - this._requestTime.getTime()) / 1000;
|
|
166
|
+
this._options.telemetryEmitter.emitTelemetryEvent({
|
|
167
|
+
type: TelemetryEventType.InlineChatResponse,
|
|
168
|
+
data: {
|
|
169
|
+
chatModel: {
|
|
170
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
171
|
+
model: NBIAPI.config.chatModel.model
|
|
172
|
+
},
|
|
173
|
+
timeElapsed
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_onRequestSubmitted(prompt: string) {
|
|
180
|
+
// code update
|
|
181
|
+
if (this._options.existingCode !== '') {
|
|
182
|
+
this.node.style.height = '300px';
|
|
183
|
+
}
|
|
184
|
+
// save the prompt in case of a rerender
|
|
185
|
+
this._options.prompt = prompt;
|
|
186
|
+
this._options.onRequestSubmitted(prompt);
|
|
187
|
+
this._requestTime = new Date();
|
|
188
|
+
this._options.telemetryEmitter.emitTelemetryEvent({
|
|
189
|
+
type: TelemetryEventType.InlineChatRequest,
|
|
190
|
+
data: {
|
|
191
|
+
chatModel: {
|
|
192
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
193
|
+
model: NBIAPI.config.chatModel.model
|
|
194
|
+
},
|
|
195
|
+
prompt: prompt
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
render(): JSX.Element {
|
|
201
|
+
return (
|
|
202
|
+
<InlinePopoverComponent
|
|
203
|
+
prompt={this._options.prompt}
|
|
204
|
+
existingCode={this._options.existingCode}
|
|
205
|
+
onRequestSubmitted={this._onRequestSubmitted.bind(this)}
|
|
206
|
+
onRequestCancelled={this._options.onRequestCancelled}
|
|
207
|
+
onResponseEmit={this._onResponse.bind(this)}
|
|
208
|
+
prefix={this._options.prefix}
|
|
209
|
+
suffix={this._options.suffix}
|
|
210
|
+
onUpdatedCodeChange={this._options.onUpdatedCodeChange}
|
|
211
|
+
onUpdatedCodeAccepted={this._options.onUpdatedCodeAccepted}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private _options: IInlinePromptWidgetOptions;
|
|
217
|
+
private _requestTime: Date;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export class GitHubCopilotStatusBarItem extends ReactWidget {
|
|
221
|
+
constructor(options: { getApp: () => JupyterFrontEnd }) {
|
|
222
|
+
super();
|
|
223
|
+
|
|
224
|
+
this._getApp = options.getApp;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(): JSX.Element {
|
|
228
|
+
return <GitHubCopilotStatusComponent getApp={this._getApp} />;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private _getApp: () => JupyterFrontEnd;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class GitHubCopilotLoginDialogBody extends ReactWidget {
|
|
235
|
+
constructor(options: { onLoggedIn: () => void }) {
|
|
236
|
+
super();
|
|
237
|
+
|
|
238
|
+
this._onLoggedIn = options.onLoggedIn;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
render(): JSX.Element {
|
|
242
|
+
return (
|
|
243
|
+
<GitHubCopilotLoginDialogBodyComponent
|
|
244
|
+
onLoggedIn={() => this._onLoggedIn()}
|
|
245
|
+
/>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private _onLoggedIn: () => void;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export class ConfigurationDialogBody extends ReactWidget {
|
|
253
|
+
constructor(options: { onSave: () => void }) {
|
|
254
|
+
super();
|
|
255
|
+
|
|
256
|
+
this._onSave = options.onSave;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
render(): JSX.Element {
|
|
260
|
+
return <ConfigurationDialogBodyComponent onSave={this._onSave} />;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private _onSave: () => void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface IChatMessageContent {
|
|
267
|
+
id: string;
|
|
268
|
+
type: ResponseStreamDataType;
|
|
269
|
+
content: any;
|
|
270
|
+
created: Date;
|
|
271
|
+
reasoningContent?: string;
|
|
272
|
+
reasoningFinished?: boolean;
|
|
273
|
+
reasoningTime?: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface IChatMessage {
|
|
277
|
+
id: string;
|
|
278
|
+
parentId?: string;
|
|
279
|
+
date: Date;
|
|
280
|
+
from: string; // 'user' | 'copilot';
|
|
281
|
+
contents: IChatMessageContent[];
|
|
282
|
+
notebookLink?: string;
|
|
283
|
+
participant?: IChatParticipant;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const answeredForms = new Map<string, string>();
|
|
287
|
+
|
|
288
|
+
function ChatResponseHTMLFrame(props: any) {
|
|
289
|
+
const iframSrc = useMemo(
|
|
290
|
+
() => URL.createObjectURL(new Blob([props.source], { type: 'text/html' })),
|
|
291
|
+
[]
|
|
292
|
+
);
|
|
293
|
+
return (
|
|
294
|
+
<div className="chat-response-html-frame" key={`key-${props.index}`}>
|
|
295
|
+
<iframe
|
|
296
|
+
className="chat-response-html-frame-iframe"
|
|
297
|
+
height={props.height}
|
|
298
|
+
sandbox="allow-scripts"
|
|
299
|
+
src={iframSrc}
|
|
300
|
+
></iframe>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ChatResponse(props: any) {
|
|
306
|
+
const [renderCount, setRenderCount] = useState(0);
|
|
307
|
+
const msg: IChatMessage = props.message;
|
|
308
|
+
const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
|
|
309
|
+
|
|
310
|
+
const openNotebook = (event: any) => {
|
|
311
|
+
const notebookPath = event.target.dataset['ref'];
|
|
312
|
+
props.openFile(notebookPath);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const markFormConfirmed = (messageId: string) => {
|
|
316
|
+
answeredForms.set(messageId, 'confirmed');
|
|
317
|
+
};
|
|
318
|
+
const markFormCanceled = (messageId: string) => {
|
|
319
|
+
answeredForms.set(messageId, 'canceled');
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const runCommand = (commandId: string, args: any) => {
|
|
323
|
+
props.getApp().commands.execute(commandId, args);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// group messages by type
|
|
327
|
+
const groupedContents: IChatMessageContent[] = [];
|
|
328
|
+
let lastItemType: ResponseStreamDataType | undefined;
|
|
329
|
+
|
|
330
|
+
const extractReasoningContent = (item: IChatMessageContent) => {
|
|
331
|
+
let currentContent = item.content as string;
|
|
332
|
+
if (typeof currentContent !== 'string') {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let reasoningContent = '';
|
|
337
|
+
let reasoningStartTime = new Date();
|
|
338
|
+
const reasoningEndTime = new Date();
|
|
339
|
+
|
|
340
|
+
const startPos = currentContent.indexOf('<think>');
|
|
341
|
+
|
|
342
|
+
const hasStart = startPos >= 0;
|
|
343
|
+
reasoningStartTime = new Date(item.created);
|
|
344
|
+
|
|
345
|
+
if (hasStart) {
|
|
346
|
+
currentContent = currentContent.substring(startPos + 7);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const endPos = currentContent.indexOf('</think>');
|
|
350
|
+
const hasEnd = endPos >= 0;
|
|
351
|
+
|
|
352
|
+
if (hasEnd) {
|
|
353
|
+
reasoningContent += currentContent.substring(0, endPos);
|
|
354
|
+
currentContent = currentContent.substring(endPos + 8);
|
|
355
|
+
} else {
|
|
356
|
+
if (hasStart) {
|
|
357
|
+
reasoningContent += currentContent;
|
|
358
|
+
currentContent = '';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
item.content = currentContent;
|
|
363
|
+
item.reasoningContent = reasoningContent;
|
|
364
|
+
item.reasoningFinished = hasEnd;
|
|
365
|
+
item.reasoningTime =
|
|
366
|
+
(reasoningEndTime.getTime() - reasoningStartTime.getTime()) / 1000;
|
|
367
|
+
|
|
368
|
+
return hasStart && !hasEnd; // is thinking
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < msg.contents.length; i++) {
|
|
372
|
+
const item = msg.contents[i];
|
|
373
|
+
if (
|
|
374
|
+
item.type === lastItemType &&
|
|
375
|
+
lastItemType === ResponseStreamDataType.MarkdownPart
|
|
376
|
+
) {
|
|
377
|
+
const lastItem = groupedContents[groupedContents.length - 1];
|
|
378
|
+
lastItem.content += item.content;
|
|
379
|
+
} else {
|
|
380
|
+
groupedContents.push(structuredClone(item));
|
|
381
|
+
lastItemType = item.type;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const [thinkingInProgress, setThinkingInProgress] = useState(false);
|
|
386
|
+
|
|
387
|
+
for (const item of groupedContents) {
|
|
388
|
+
const isThinking = extractReasoningContent(item);
|
|
389
|
+
if (isThinking && !thinkingInProgress) {
|
|
390
|
+
setThinkingInProgress(true);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
let intervalId: any = undefined;
|
|
396
|
+
if (thinkingInProgress) {
|
|
397
|
+
intervalId = setInterval(() => {
|
|
398
|
+
setRenderCount(prev => prev + 1);
|
|
399
|
+
setThinkingInProgress(false);
|
|
400
|
+
}, 1000);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return () => clearInterval(intervalId);
|
|
404
|
+
}, [thinkingInProgress]);
|
|
405
|
+
|
|
406
|
+
const onExpandCollapseClick = (event: any) => {
|
|
407
|
+
const parent = event.currentTarget.parentElement;
|
|
408
|
+
if (parent.classList.contains('expanded')) {
|
|
409
|
+
parent.classList.remove('expanded');
|
|
410
|
+
} else {
|
|
411
|
+
parent.classList.add('expanded');
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<div
|
|
417
|
+
className={`chat-message chat-message-${msg.from}`}
|
|
418
|
+
data-render-count={renderCount}
|
|
419
|
+
>
|
|
420
|
+
<div className="chat-message-header">
|
|
421
|
+
<div className="chat-message-from">
|
|
422
|
+
{msg.participant?.iconPath && (
|
|
423
|
+
<div
|
|
424
|
+
className={`chat-message-from-icon ${msg.participant?.id === 'default' ? 'chat-message-from-icon-default' : ''} ${isDarkTheme() ? 'dark' : ''}`}
|
|
425
|
+
>
|
|
426
|
+
<img src={msg.participant.iconPath} />
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
<div className="chat-message-from-title">
|
|
430
|
+
{msg.from === 'user' ? 'User' : msg.participant?.name || 'Copilot'}
|
|
431
|
+
</div>
|
|
432
|
+
<div
|
|
433
|
+
className="chat-message-from-progress"
|
|
434
|
+
style={{ display: `${props.showGenerating ? 'visible' : 'none'}` }}
|
|
435
|
+
>
|
|
436
|
+
<div className="loading-ellipsis">Generating</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
<div className="chat-message-timestamp">{timestamp}</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div className="chat-message-content">
|
|
442
|
+
{groupedContents.map((item, index) => {
|
|
443
|
+
switch (item.type) {
|
|
444
|
+
case ResponseStreamDataType.Markdown:
|
|
445
|
+
case ResponseStreamDataType.MarkdownPart:
|
|
446
|
+
return (
|
|
447
|
+
<>
|
|
448
|
+
{item.reasoningContent && (
|
|
449
|
+
<div className="chat-reasoning-content">
|
|
450
|
+
<div
|
|
451
|
+
className="chat-reasoning-content-title"
|
|
452
|
+
onClick={(event: any) => onExpandCollapseClick(event)}
|
|
453
|
+
>
|
|
454
|
+
<VscTriangleRight className="collapsed-icon"></VscTriangleRight>
|
|
455
|
+
<VscTriangleDown className="expanded-icon"></VscTriangleDown>{' '}
|
|
456
|
+
{item.reasoningFinished
|
|
457
|
+
? 'Thought'
|
|
458
|
+
: `Thinking (${Math.floor(item.reasoningTime)} s)`}
|
|
459
|
+
</div>
|
|
460
|
+
<div className="chat-reasoning-content-text">
|
|
461
|
+
<MarkdownRenderer
|
|
462
|
+
key={`key-${index}`}
|
|
463
|
+
getApp={props.getApp}
|
|
464
|
+
getActiveDocumentInfo={props.getActiveDocumentInfo}
|
|
465
|
+
>
|
|
466
|
+
{item.reasoningContent}
|
|
467
|
+
</MarkdownRenderer>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
<MarkdownRenderer
|
|
472
|
+
key={`key-${index}`}
|
|
473
|
+
getApp={props.getApp}
|
|
474
|
+
getActiveDocumentInfo={props.getActiveDocumentInfo}
|
|
475
|
+
>
|
|
476
|
+
{item.content}
|
|
477
|
+
</MarkdownRenderer>
|
|
478
|
+
</>
|
|
479
|
+
);
|
|
480
|
+
case ResponseStreamDataType.HTMLFrame:
|
|
481
|
+
return (
|
|
482
|
+
<ChatResponseHTMLFrame
|
|
483
|
+
index={index}
|
|
484
|
+
source={item.content.source}
|
|
485
|
+
height={item.content.height}
|
|
486
|
+
/>
|
|
487
|
+
);
|
|
488
|
+
case ResponseStreamDataType.Button:
|
|
489
|
+
return (
|
|
490
|
+
<div className="chat-response-button">
|
|
491
|
+
<button
|
|
492
|
+
key={`key-${index}`}
|
|
493
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
494
|
+
onClick={() =>
|
|
495
|
+
runCommand(item.content.commandId, item.content.args)
|
|
496
|
+
}
|
|
497
|
+
>
|
|
498
|
+
<div className="jp-Dialog-buttonLabel">
|
|
499
|
+
{item.content.title}
|
|
500
|
+
</div>
|
|
501
|
+
</button>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
case ResponseStreamDataType.Anchor:
|
|
505
|
+
return (
|
|
506
|
+
<div className="chat-response-anchor">
|
|
507
|
+
<a
|
|
508
|
+
key={`key-${index}`}
|
|
509
|
+
href={item.content.uri}
|
|
510
|
+
target="_blank"
|
|
511
|
+
>
|
|
512
|
+
{item.content.title}
|
|
513
|
+
</a>
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
case ResponseStreamDataType.Progress:
|
|
517
|
+
// show only if no more message available
|
|
518
|
+
return index === groupedContents.length - 1 ? (
|
|
519
|
+
<div key={`key-${index}`}>✓ {item.content}</div>
|
|
520
|
+
) : null;
|
|
521
|
+
case ResponseStreamDataType.Confirmation:
|
|
522
|
+
return answeredForms.get(item.id) ===
|
|
523
|
+
'confirmed' ? null : answeredForms.get(item.id) ===
|
|
524
|
+
'canceled' ? (
|
|
525
|
+
<div>✖ Canceled</div>
|
|
526
|
+
) : (
|
|
527
|
+
<div className="chat-confirmation-form" key={`key-${index}`}>
|
|
528
|
+
{item.content.title ? (
|
|
529
|
+
<div>
|
|
530
|
+
<b>{item.content.title}</b>
|
|
531
|
+
</div>
|
|
532
|
+
) : null}
|
|
533
|
+
{item.content.message ? (
|
|
534
|
+
<div>{item.content.message}</div>
|
|
535
|
+
) : null}
|
|
536
|
+
<button
|
|
537
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
538
|
+
onClick={() => {
|
|
539
|
+
markFormConfirmed(item.id);
|
|
540
|
+
runCommand(
|
|
541
|
+
'notebook-intelligence:chat-user-input',
|
|
542
|
+
item.content.confirmArgs
|
|
543
|
+
);
|
|
544
|
+
}}
|
|
545
|
+
>
|
|
546
|
+
<div className="jp-Dialog-buttonLabel">
|
|
547
|
+
{item.content.confirmLabel}
|
|
548
|
+
</div>
|
|
549
|
+
</button>
|
|
550
|
+
<button
|
|
551
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
552
|
+
onClick={() => {
|
|
553
|
+
markFormCanceled(item.id);
|
|
554
|
+
runCommand(
|
|
555
|
+
'notebook-intelligence:chat-user-input',
|
|
556
|
+
item.content.cancelArgs
|
|
557
|
+
);
|
|
558
|
+
}}
|
|
559
|
+
>
|
|
560
|
+
<div className="jp-Dialog-buttonLabel">
|
|
561
|
+
{item.content.cancelLabel}
|
|
562
|
+
</div>
|
|
563
|
+
</button>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
})}
|
|
569
|
+
|
|
570
|
+
{msg.notebookLink && (
|
|
571
|
+
<a
|
|
572
|
+
className="copilot-generated-notebook-link"
|
|
573
|
+
data-ref={msg.notebookLink}
|
|
574
|
+
onClick={openNotebook}
|
|
575
|
+
>
|
|
576
|
+
open notebook
|
|
577
|
+
</a>
|
|
578
|
+
)}
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function submitCompletionRequest(
|
|
585
|
+
request: IRunChatCompletionRequest,
|
|
586
|
+
responseEmitter: IChatCompletionResponseEmitter
|
|
587
|
+
): Promise<any> {
|
|
588
|
+
switch (request.type) {
|
|
589
|
+
case RunChatCompletionType.Chat:
|
|
590
|
+
return NBIAPI.chatRequest(
|
|
591
|
+
request.messageId,
|
|
592
|
+
request.chatId,
|
|
593
|
+
request.content,
|
|
594
|
+
request.language || 'python',
|
|
595
|
+
request.filename || 'Untitled.ipynb',
|
|
596
|
+
request.additionalContext || [],
|
|
597
|
+
responseEmitter
|
|
598
|
+
);
|
|
599
|
+
case RunChatCompletionType.ExplainThis:
|
|
600
|
+
case RunChatCompletionType.FixThis:
|
|
601
|
+
case RunChatCompletionType.ExplainThisOutput:
|
|
602
|
+
case RunChatCompletionType.TroubleshootThisOutput: {
|
|
603
|
+
return NBIAPI.chatRequest(
|
|
604
|
+
request.messageId,
|
|
605
|
+
request.chatId,
|
|
606
|
+
request.content,
|
|
607
|
+
request.language || 'python',
|
|
608
|
+
request.filename || 'Untitled.ipynb',
|
|
609
|
+
[],
|
|
610
|
+
responseEmitter
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
case RunChatCompletionType.GenerateCode:
|
|
614
|
+
return NBIAPI.generateCode(
|
|
615
|
+
request.chatId,
|
|
616
|
+
request.content,
|
|
617
|
+
request.prefix || '',
|
|
618
|
+
request.suffix || '',
|
|
619
|
+
request.existingCode || '',
|
|
620
|
+
request.language || 'python',
|
|
621
|
+
request.filename || 'Untitled.ipynb',
|
|
622
|
+
responseEmitter
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function SidebarComponent(props: any) {
|
|
628
|
+
const [chatMessages, setChatMessages] = useState<IChatMessage[]>([]);
|
|
629
|
+
const [prompt, setPrompt] = useState<string>('');
|
|
630
|
+
const [draftPrompt, setDraftPrompt] = useState<string>('');
|
|
631
|
+
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
|
632
|
+
const [ghLoginStatus, setGHLoginStatus] = useState(
|
|
633
|
+
GitHubCopilotLoginStatus.NotLoggedIn
|
|
634
|
+
);
|
|
635
|
+
const [loginClickCount, _setLoginClickCount] = useState(0);
|
|
636
|
+
const [copilotRequestInProgress, setCopilotRequestInProgress] =
|
|
637
|
+
useState(false);
|
|
638
|
+
const [showPopover, setShowPopover] = useState(false);
|
|
639
|
+
const [originalPrefixes, setOriginalPrefixes] = useState<string[]>([]);
|
|
640
|
+
const [prefixSuggestions, setPrefixSuggestions] = useState<string[]>([]);
|
|
641
|
+
const [selectedPrefixSuggestionIndex, setSelectedPrefixSuggestionIndex] =
|
|
642
|
+
useState(0);
|
|
643
|
+
const promptInputRef = useRef<HTMLTextAreaElement>(null);
|
|
644
|
+
const [promptHistory, setPromptHistory] = useState<string[]>([]);
|
|
645
|
+
// position on prompt history stack
|
|
646
|
+
const [promptHistoryIndex, setPromptHistoryIndex] = useState(0);
|
|
647
|
+
const [chatId, setChatId] = useState(UUID.uuid4());
|
|
648
|
+
const lastMessageId = useRef<string>('');
|
|
649
|
+
const lastRequestTime = useRef<Date>(new Date());
|
|
650
|
+
const [contextOn, setContextOn] = useState(false);
|
|
651
|
+
const [activeDocumentInfo, setActiveDocumentInfo] =
|
|
652
|
+
useState<IActiveDocumentInfo | null>(null);
|
|
653
|
+
const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
|
|
654
|
+
const telemetryEmitter: ITelemetryEmitter = props.getTelemetryEmitter();
|
|
655
|
+
|
|
656
|
+
useEffect(() => {
|
|
657
|
+
const prefixes: string[] = [];
|
|
658
|
+
const chatParticipants = NBIAPI.config.chatParticipants;
|
|
659
|
+
for (const participant of chatParticipants) {
|
|
660
|
+
const id = participant.id;
|
|
661
|
+
const commands = participant.commands;
|
|
662
|
+
const participantPrefix = id === 'default' ? '' : `@${id}`;
|
|
663
|
+
if (participantPrefix !== '') {
|
|
664
|
+
prefixes.push(participantPrefix);
|
|
665
|
+
}
|
|
666
|
+
const commandPrefix =
|
|
667
|
+
participantPrefix === '' ? '' : `${participantPrefix} `;
|
|
668
|
+
for (const command of commands) {
|
|
669
|
+
prefixes.push(`${commandPrefix}/${command}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
setOriginalPrefixes(prefixes);
|
|
673
|
+
setPrefixSuggestions(prefixes);
|
|
674
|
+
}, []);
|
|
675
|
+
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
const fetchData = () => {
|
|
678
|
+
setGHLoginStatus(NBIAPI.getLoginStatus());
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
fetchData();
|
|
682
|
+
|
|
683
|
+
const intervalId = setInterval(fetchData, 1000);
|
|
684
|
+
|
|
685
|
+
return () => clearInterval(intervalId);
|
|
686
|
+
}, [loginClickCount]);
|
|
687
|
+
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
setSelectedPrefixSuggestionIndex(0);
|
|
690
|
+
}, [prefixSuggestions]);
|
|
691
|
+
|
|
692
|
+
const onPromptChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
693
|
+
const newPrompt = event.target.value;
|
|
694
|
+
setPrompt(newPrompt);
|
|
695
|
+
const trimmedPrompt = newPrompt.trimStart();
|
|
696
|
+
if (trimmedPrompt === '@' || trimmedPrompt === '/') {
|
|
697
|
+
setShowPopover(true);
|
|
698
|
+
filterPrefixSuggestions(trimmedPrompt);
|
|
699
|
+
} else if (
|
|
700
|
+
trimmedPrompt.startsWith('@') ||
|
|
701
|
+
trimmedPrompt.startsWith('/') ||
|
|
702
|
+
trimmedPrompt === ''
|
|
703
|
+
) {
|
|
704
|
+
filterPrefixSuggestions(trimmedPrompt);
|
|
705
|
+
} else {
|
|
706
|
+
setShowPopover(false);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const applyPrefixSuggestion = (prefix: string) => {
|
|
711
|
+
if (prefix.includes(prompt)) {
|
|
712
|
+
setPrompt(`${prefix} `);
|
|
713
|
+
} else {
|
|
714
|
+
setPrompt(`${prefix} ${prompt} `);
|
|
715
|
+
}
|
|
716
|
+
setShowPopover(false);
|
|
717
|
+
promptInputRef.current?.focus();
|
|
718
|
+
setSelectedPrefixSuggestionIndex(0);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const prefixSuggestionSelected = (event: any) => {
|
|
722
|
+
const prefix = event.target.dataset['value'];
|
|
723
|
+
applyPrefixSuggestion(prefix);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const handleSubmitStopChatButtonClick = async () => {
|
|
727
|
+
if (!copilotRequestInProgress) {
|
|
728
|
+
handleUserInputSubmit();
|
|
729
|
+
} else {
|
|
730
|
+
handleUserInputCancel();
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const handleUserInputSubmit = async () => {
|
|
735
|
+
setPromptHistoryIndex(promptHistory.length + 1);
|
|
736
|
+
setPromptHistory([...promptHistory, prompt]);
|
|
737
|
+
setShowPopover(false);
|
|
738
|
+
|
|
739
|
+
const promptPrefixParts = [];
|
|
740
|
+
const promptParts = prompt.split(' ');
|
|
741
|
+
if (promptParts.length > 1) {
|
|
742
|
+
for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
|
|
743
|
+
const part = promptParts[i];
|
|
744
|
+
if (part.startsWith('@') || part.startsWith('/')) {
|
|
745
|
+
promptPrefixParts.push(part);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const promptPrefix =
|
|
751
|
+
promptPrefixParts.length > 0 ? promptPrefixParts.join(' ') + ' ' : '';
|
|
752
|
+
|
|
753
|
+
lastMessageId.current = UUID.uuid4();
|
|
754
|
+
lastRequestTime.current = new Date();
|
|
755
|
+
|
|
756
|
+
const newList = [
|
|
757
|
+
...chatMessages,
|
|
758
|
+
{
|
|
759
|
+
id: lastMessageId.current,
|
|
760
|
+
date: new Date(),
|
|
761
|
+
from: 'user',
|
|
762
|
+
contents: [
|
|
763
|
+
{
|
|
764
|
+
id: lastMessageId.current,
|
|
765
|
+
type: ResponseStreamDataType.Markdown,
|
|
766
|
+
content: prompt,
|
|
767
|
+
created: new Date()
|
|
768
|
+
}
|
|
769
|
+
]
|
|
770
|
+
}
|
|
771
|
+
];
|
|
772
|
+
setChatMessages(newList);
|
|
773
|
+
|
|
774
|
+
if (prompt.startsWith('/clear')) {
|
|
775
|
+
setChatMessages([]);
|
|
776
|
+
setPrompt('');
|
|
777
|
+
resetChatId();
|
|
778
|
+
resetPrefixSuggestions();
|
|
779
|
+
setPromptHistory([]);
|
|
780
|
+
setPromptHistoryIndex(0);
|
|
781
|
+
NBIAPI.sendWebSocketMessage(
|
|
782
|
+
UUID.uuid4(),
|
|
783
|
+
RequestDataType.ClearChatHistory,
|
|
784
|
+
{ chatId }
|
|
785
|
+
);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
setCopilotRequestInProgress(true);
|
|
790
|
+
|
|
791
|
+
const activeDocInfo: IActiveDocumentInfo = props.getActiveDocumentInfo();
|
|
792
|
+
const extractedPrompt = prompt;
|
|
793
|
+
const contents: IChatMessageContent[] = [];
|
|
794
|
+
const app = props.getApp();
|
|
795
|
+
const additionalContext: IContextItem[] = [];
|
|
796
|
+
if (contextOn && activeDocumentInfo?.filename) {
|
|
797
|
+
const selection = activeDocumentInfo.selection;
|
|
798
|
+
const textSelected =
|
|
799
|
+
selection &&
|
|
800
|
+
!(
|
|
801
|
+
selection.start.line === selection.end.line &&
|
|
802
|
+
selection.start.column === selection.end.column
|
|
803
|
+
);
|
|
804
|
+
additionalContext.push({
|
|
805
|
+
type: ContextType.CurrentFile,
|
|
806
|
+
content: props.getActiveSelectionContent(),
|
|
807
|
+
currentCellContents: textSelected
|
|
808
|
+
? null
|
|
809
|
+
: props.getCurrentCellContents(),
|
|
810
|
+
filePath: activeDocumentInfo.filePath,
|
|
811
|
+
cellIndex: activeDocumentInfo.activeCellIndex,
|
|
812
|
+
startLine: selection ? selection.start.line + 1 : 1,
|
|
813
|
+
endLine: selection ? selection.end.line + 1 : 1
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
submitCompletionRequest(
|
|
818
|
+
{
|
|
819
|
+
messageId: lastMessageId.current,
|
|
820
|
+
chatId,
|
|
821
|
+
type: RunChatCompletionType.Chat,
|
|
822
|
+
content: extractedPrompt,
|
|
823
|
+
language: activeDocInfo.language,
|
|
824
|
+
filename: activeDocInfo.filename,
|
|
825
|
+
additionalContext
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
emit: async response => {
|
|
829
|
+
if (response.id !== lastMessageId.current) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let responseMessage = '';
|
|
834
|
+
if (response.type === BackendMessageType.StreamMessage) {
|
|
835
|
+
const delta = response.data['choices']?.[0]?.['delta'];
|
|
836
|
+
if (!delta) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (delta['nbiContent']) {
|
|
840
|
+
const nbiContent = delta['nbiContent'];
|
|
841
|
+
contents.push({
|
|
842
|
+
id: response.id,
|
|
843
|
+
type: nbiContent.type,
|
|
844
|
+
content: nbiContent.content,
|
|
845
|
+
created: new Date(response.created)
|
|
846
|
+
});
|
|
847
|
+
} else {
|
|
848
|
+
responseMessage =
|
|
849
|
+
response.data['choices']?.[0]?.['delta']?.['content'];
|
|
850
|
+
if (!responseMessage) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
contents.push({
|
|
854
|
+
id: response.id,
|
|
855
|
+
type: ResponseStreamDataType.MarkdownPart,
|
|
856
|
+
content: responseMessage,
|
|
857
|
+
created: new Date(response.created)
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
} else if (response.type === BackendMessageType.StreamEnd) {
|
|
861
|
+
setCopilotRequestInProgress(false);
|
|
862
|
+
const timeElapsed =
|
|
863
|
+
(new Date().getTime() - lastRequestTime.current.getTime()) / 1000;
|
|
864
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
865
|
+
type: TelemetryEventType.ChatResponse,
|
|
866
|
+
data: {
|
|
867
|
+
chatModel: {
|
|
868
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
869
|
+
model: NBIAPI.config.chatModel.model
|
|
870
|
+
},
|
|
871
|
+
timeElapsed
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
} else if (response.type === BackendMessageType.RunUICommand) {
|
|
875
|
+
const messageId = response.id;
|
|
876
|
+
const result = await app.commands.execute(
|
|
877
|
+
response.data.commandId,
|
|
878
|
+
response.data.args
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
const data = {
|
|
882
|
+
callback_id: response.data.callback_id,
|
|
883
|
+
result: result || 'void'
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
JSON.stringify(data);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
data.result = 'Could not serialize the result';
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
NBIAPI.sendWebSocketMessage(
|
|
893
|
+
messageId,
|
|
894
|
+
RequestDataType.RunUICommandResponse,
|
|
895
|
+
data
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
setChatMessages([
|
|
899
|
+
...newList,
|
|
900
|
+
{
|
|
901
|
+
id: UUID.uuid4(),
|
|
902
|
+
date: new Date(),
|
|
903
|
+
from: 'copilot',
|
|
904
|
+
contents: contents,
|
|
905
|
+
participant: NBIAPI.config.chatParticipants.find(participant => {
|
|
906
|
+
return participant.id === response.participant;
|
|
907
|
+
})
|
|
908
|
+
}
|
|
909
|
+
]);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
const newPrompt = prompt.startsWith('/settings') ? '' : promptPrefix;
|
|
915
|
+
|
|
916
|
+
setPrompt(newPrompt);
|
|
917
|
+
filterPrefixSuggestions(newPrompt);
|
|
918
|
+
|
|
919
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
920
|
+
type: TelemetryEventType.ChatRequest,
|
|
921
|
+
data: {
|
|
922
|
+
chatModel: {
|
|
923
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
924
|
+
model: NBIAPI.config.chatModel.model
|
|
925
|
+
},
|
|
926
|
+
prompt: extractedPrompt
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const handleUserInputCancel = async () => {
|
|
932
|
+
NBIAPI.sendWebSocketMessage(
|
|
933
|
+
lastMessageId.current,
|
|
934
|
+
RequestDataType.CancelChatRequest,
|
|
935
|
+
{ chatId }
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
lastMessageId.current = '';
|
|
939
|
+
setCopilotRequestInProgress(false);
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const filterPrefixSuggestions = (prmpt: string) => {
|
|
943
|
+
const userInput = prmpt.trimStart();
|
|
944
|
+
if (userInput === '') {
|
|
945
|
+
setPrefixSuggestions(originalPrefixes);
|
|
946
|
+
} else {
|
|
947
|
+
setPrefixSuggestions(
|
|
948
|
+
originalPrefixes.filter(prefix => prefix.includes(userInput))
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const resetPrefixSuggestions = () => {
|
|
954
|
+
setPrefixSuggestions(originalPrefixes);
|
|
955
|
+
setSelectedPrefixSuggestionIndex(0);
|
|
956
|
+
};
|
|
957
|
+
const resetChatId = () => {
|
|
958
|
+
setChatId(UUID.uuid4());
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const onPromptKeyDown = async (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
962
|
+
if (event.key === 'Enter') {
|
|
963
|
+
event.stopPropagation();
|
|
964
|
+
event.preventDefault();
|
|
965
|
+
if (showPopover) {
|
|
966
|
+
applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
setSelectedPrefixSuggestionIndex(0);
|
|
971
|
+
handleSubmitStopChatButtonClick();
|
|
972
|
+
} else if (event.key === 'Tab') {
|
|
973
|
+
if (showPopover) {
|
|
974
|
+
event.stopPropagation();
|
|
975
|
+
event.preventDefault();
|
|
976
|
+
applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
} else if (event.key === 'Escape') {
|
|
980
|
+
event.stopPropagation();
|
|
981
|
+
event.preventDefault();
|
|
982
|
+
setShowPopover(false);
|
|
983
|
+
setSelectedPrefixSuggestionIndex(0);
|
|
984
|
+
} else if (event.key === 'ArrowUp') {
|
|
985
|
+
event.stopPropagation();
|
|
986
|
+
event.preventDefault();
|
|
987
|
+
|
|
988
|
+
if (showPopover) {
|
|
989
|
+
setSelectedPrefixSuggestionIndex(
|
|
990
|
+
(selectedPrefixSuggestionIndex - 1 + prefixSuggestions.length) %
|
|
991
|
+
prefixSuggestions.length
|
|
992
|
+
);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
setShowPopover(false);
|
|
997
|
+
// first time up key press
|
|
998
|
+
if (
|
|
999
|
+
promptHistory.length > 0 &&
|
|
1000
|
+
promptHistoryIndex === promptHistory.length
|
|
1001
|
+
) {
|
|
1002
|
+
setDraftPrompt(prompt);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (
|
|
1006
|
+
promptHistory.length > 0 &&
|
|
1007
|
+
promptHistoryIndex > 0 &&
|
|
1008
|
+
promptHistoryIndex <= promptHistory.length
|
|
1009
|
+
) {
|
|
1010
|
+
const prevPrompt = promptHistory[promptHistoryIndex - 1];
|
|
1011
|
+
const newIndex = promptHistoryIndex - 1;
|
|
1012
|
+
setPrompt(prevPrompt);
|
|
1013
|
+
setPromptHistoryIndex(newIndex);
|
|
1014
|
+
}
|
|
1015
|
+
} else if (event.key === 'ArrowDown') {
|
|
1016
|
+
event.stopPropagation();
|
|
1017
|
+
event.preventDefault();
|
|
1018
|
+
|
|
1019
|
+
if (showPopover) {
|
|
1020
|
+
setSelectedPrefixSuggestionIndex(
|
|
1021
|
+
(selectedPrefixSuggestionIndex + 1 + prefixSuggestions.length) %
|
|
1022
|
+
prefixSuggestions.length
|
|
1023
|
+
);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
setShowPopover(false);
|
|
1028
|
+
if (
|
|
1029
|
+
promptHistory.length > 0 &&
|
|
1030
|
+
promptHistoryIndex >= 0 &&
|
|
1031
|
+
promptHistoryIndex < promptHistory.length
|
|
1032
|
+
) {
|
|
1033
|
+
if (promptHistoryIndex === promptHistory.length - 1) {
|
|
1034
|
+
setPrompt(draftPrompt);
|
|
1035
|
+
setPromptHistoryIndex(promptHistory.length);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const prevPrompt = promptHistory[promptHistoryIndex + 1];
|
|
1039
|
+
const newIndex = promptHistoryIndex + 1;
|
|
1040
|
+
setPrompt(prevPrompt);
|
|
1041
|
+
setPromptHistoryIndex(newIndex);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
const scrollMessagesToBottom = () => {
|
|
1047
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const handleConfigurationClick = async () => {
|
|
1051
|
+
props
|
|
1052
|
+
.getApp()
|
|
1053
|
+
.commands.execute('notebook-intelligence:open-configuration-dialog');
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const handleLoginClick = async () => {
|
|
1057
|
+
props
|
|
1058
|
+
.getApp()
|
|
1059
|
+
.commands.execute(
|
|
1060
|
+
'notebook-intelligence:open-github-copilot-login-dialog'
|
|
1061
|
+
);
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
useEffect(() => {
|
|
1065
|
+
scrollMessagesToBottom();
|
|
1066
|
+
}, [chatMessages]);
|
|
1067
|
+
|
|
1068
|
+
const promptRequestHandler = useCallback(
|
|
1069
|
+
(eventData: any) => {
|
|
1070
|
+
const request: IRunChatCompletionRequest = eventData.detail;
|
|
1071
|
+
request.chatId = chatId;
|
|
1072
|
+
let message = '';
|
|
1073
|
+
switch (request.type) {
|
|
1074
|
+
case RunChatCompletionType.ExplainThis:
|
|
1075
|
+
message = `Explain this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
|
|
1076
|
+
break;
|
|
1077
|
+
case RunChatCompletionType.FixThis:
|
|
1078
|
+
message = `Fix this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
|
|
1079
|
+
break;
|
|
1080
|
+
case RunChatCompletionType.ExplainThisOutput:
|
|
1081
|
+
message = `Explain this notebook cell output: \n\`\`\`\n${request.content}\n\`\`\`\n`;
|
|
1082
|
+
break;
|
|
1083
|
+
case RunChatCompletionType.TroubleshootThisOutput:
|
|
1084
|
+
message = `Troubleshoot errors reported in the notebook cell output: \n\`\`\`\n${request.content}\n\`\`\`\n`;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
const messageId = UUID.uuid4();
|
|
1088
|
+
request.messageId = messageId;
|
|
1089
|
+
const newList = [
|
|
1090
|
+
...chatMessages,
|
|
1091
|
+
{
|
|
1092
|
+
id: messageId,
|
|
1093
|
+
date: new Date(),
|
|
1094
|
+
from: 'user',
|
|
1095
|
+
contents: [
|
|
1096
|
+
{
|
|
1097
|
+
id: messageId,
|
|
1098
|
+
type: ResponseStreamDataType.Markdown,
|
|
1099
|
+
content: message,
|
|
1100
|
+
created: new Date()
|
|
1101
|
+
}
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
];
|
|
1105
|
+
setChatMessages(newList);
|
|
1106
|
+
|
|
1107
|
+
setCopilotRequestInProgress(true);
|
|
1108
|
+
|
|
1109
|
+
const contents: IChatMessageContent[] = [];
|
|
1110
|
+
|
|
1111
|
+
submitCompletionRequest(request, {
|
|
1112
|
+
emit: response => {
|
|
1113
|
+
if (response.type === BackendMessageType.StreamMessage) {
|
|
1114
|
+
const delta = response.data['choices']?.[0]?.['delta'];
|
|
1115
|
+
if (!delta) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const responseMessage =
|
|
1120
|
+
response.data['choices']?.[0]?.['delta']?.['content'];
|
|
1121
|
+
if (!responseMessage) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
contents.push({
|
|
1125
|
+
id: response.id,
|
|
1126
|
+
type: ResponseStreamDataType.MarkdownPart,
|
|
1127
|
+
content: responseMessage,
|
|
1128
|
+
created: new Date(response.created)
|
|
1129
|
+
});
|
|
1130
|
+
} else if (response.type === BackendMessageType.StreamEnd) {
|
|
1131
|
+
setCopilotRequestInProgress(false);
|
|
1132
|
+
}
|
|
1133
|
+
const messageId = UUID.uuid4();
|
|
1134
|
+
setChatMessages([
|
|
1135
|
+
...newList,
|
|
1136
|
+
{
|
|
1137
|
+
id: messageId,
|
|
1138
|
+
date: new Date(),
|
|
1139
|
+
from: 'copilot',
|
|
1140
|
+
contents: contents,
|
|
1141
|
+
participant: NBIAPI.config.chatParticipants.find(participant => {
|
|
1142
|
+
return participant.id === response.participant;
|
|
1143
|
+
})
|
|
1144
|
+
}
|
|
1145
|
+
]);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
},
|
|
1149
|
+
[chatMessages]
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
useEffect(() => {
|
|
1153
|
+
document.addEventListener('copilotSidebar:runPrompt', promptRequestHandler);
|
|
1154
|
+
|
|
1155
|
+
return () => {
|
|
1156
|
+
document.removeEventListener(
|
|
1157
|
+
'copilotSidebar:runPrompt',
|
|
1158
|
+
promptRequestHandler
|
|
1159
|
+
);
|
|
1160
|
+
};
|
|
1161
|
+
}, [chatMessages]);
|
|
1162
|
+
|
|
1163
|
+
const activeDocumentChangeHandler = (eventData: any) => {
|
|
1164
|
+
// if file changes reset the context toggle
|
|
1165
|
+
if (
|
|
1166
|
+
eventData.detail.activeDocumentInfo?.filePath !==
|
|
1167
|
+
activeDocumentInfo?.filePath
|
|
1168
|
+
) {
|
|
1169
|
+
setContextOn(false);
|
|
1170
|
+
}
|
|
1171
|
+
setActiveDocumentInfo({
|
|
1172
|
+
...eventData.detail.activeDocumentInfo,
|
|
1173
|
+
...{ activeWidget: null }
|
|
1174
|
+
});
|
|
1175
|
+
setCurrentFileContextTitle(
|
|
1176
|
+
getActiveDocumentContextTitle(eventData.detail.activeDocumentInfo)
|
|
1177
|
+
);
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
useEffect(() => {
|
|
1181
|
+
document.addEventListener(
|
|
1182
|
+
'copilotSidebar:activeDocumentChanged',
|
|
1183
|
+
activeDocumentChangeHandler
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
return () => {
|
|
1187
|
+
document.removeEventListener(
|
|
1188
|
+
'copilotSidebar:activeDocumentChanged',
|
|
1189
|
+
activeDocumentChangeHandler
|
|
1190
|
+
);
|
|
1191
|
+
};
|
|
1192
|
+
}, [activeDocumentInfo]);
|
|
1193
|
+
|
|
1194
|
+
const getActiveDocumentContextTitle = (
|
|
1195
|
+
activeDocumentInfo: IActiveDocumentInfo
|
|
1196
|
+
): string => {
|
|
1197
|
+
if (!activeDocumentInfo?.filename) {
|
|
1198
|
+
return '';
|
|
1199
|
+
}
|
|
1200
|
+
const wholeFile =
|
|
1201
|
+
!activeDocumentInfo.selection ||
|
|
1202
|
+
(activeDocumentInfo.selection.start.line ===
|
|
1203
|
+
activeDocumentInfo.selection.end.line &&
|
|
1204
|
+
activeDocumentInfo.selection.start.column ===
|
|
1205
|
+
activeDocumentInfo.selection.end.column);
|
|
1206
|
+
let cellAndLineIndicator = '';
|
|
1207
|
+
|
|
1208
|
+
if (!wholeFile) {
|
|
1209
|
+
if (activeDocumentInfo.filename.endsWith('.ipynb')) {
|
|
1210
|
+
cellAndLineIndicator = ` · Cell ${activeDocumentInfo.activeCellIndex + 1}`;
|
|
1211
|
+
}
|
|
1212
|
+
if (
|
|
1213
|
+
activeDocumentInfo.selection.start.line ===
|
|
1214
|
+
activeDocumentInfo.selection.end.line
|
|
1215
|
+
) {
|
|
1216
|
+
cellAndLineIndicator += `:${activeDocumentInfo.selection.start.line + 1}`;
|
|
1217
|
+
} else {
|
|
1218
|
+
cellAndLineIndicator += `:${activeDocumentInfo.selection.start.line + 1}-${activeDocumentInfo.selection.end.line + 1}`;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return `${activeDocumentInfo.filename}${cellAndLineIndicator}`;
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const nbiConfig = NBIAPI.config;
|
|
1226
|
+
const getGHLoginRequired = () => {
|
|
1227
|
+
return (
|
|
1228
|
+
nbiConfig.usingGitHubCopilotModel &&
|
|
1229
|
+
NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn
|
|
1230
|
+
);
|
|
1231
|
+
};
|
|
1232
|
+
const getChatEnabled = () => {
|
|
1233
|
+
return nbiConfig.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
|
|
1234
|
+
? !getGHLoginRequired()
|
|
1235
|
+
: nbiConfig.llmProviders.find(
|
|
1236
|
+
provider => provider.id === nbiConfig.chatModel.provider
|
|
1237
|
+
);
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
const [ghLoginRequired, setGHLoginRequired] = useState(getGHLoginRequired());
|
|
1241
|
+
const [chatEnabled, setChatEnabled] = useState(getChatEnabled());
|
|
1242
|
+
|
|
1243
|
+
NBIAPI.configChanged.connect(() => {
|
|
1244
|
+
setGHLoginRequired(getGHLoginRequired());
|
|
1245
|
+
setChatEnabled(getChatEnabled());
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
setGHLoginRequired(getGHLoginRequired());
|
|
1250
|
+
setChatEnabled(getChatEnabled());
|
|
1251
|
+
}, [ghLoginStatus]);
|
|
1252
|
+
|
|
1253
|
+
return (
|
|
1254
|
+
<div className="sidebar">
|
|
1255
|
+
<div className="sidebar-header">
|
|
1256
|
+
<div className="sidebar-title">Copilot Chat</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
{!chatEnabled && !ghLoginRequired && (
|
|
1259
|
+
<div className="sidebar-login-info">
|
|
1260
|
+
Chat is disabled as you don't have a model configured.
|
|
1261
|
+
<button
|
|
1262
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
1263
|
+
onClick={handleConfigurationClick}
|
|
1264
|
+
>
|
|
1265
|
+
<div className="jp-Dialog-buttonLabel">Configure models</div>
|
|
1266
|
+
</button>
|
|
1267
|
+
</div>
|
|
1268
|
+
)}
|
|
1269
|
+
{ghLoginRequired && (
|
|
1270
|
+
<div className="sidebar-login-info">
|
|
1271
|
+
<div>
|
|
1272
|
+
You are not logged in to GitHub Copilot. Please login now to
|
|
1273
|
+
activate chat.
|
|
1274
|
+
</div>
|
|
1275
|
+
<div>
|
|
1276
|
+
<button
|
|
1277
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
1278
|
+
onClick={handleLoginClick}
|
|
1279
|
+
>
|
|
1280
|
+
<div className="jp-Dialog-buttonLabel">
|
|
1281
|
+
Login to GitHub Copilot
|
|
1282
|
+
</div>
|
|
1283
|
+
</button>
|
|
1284
|
+
|
|
1285
|
+
<button
|
|
1286
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
1287
|
+
onClick={handleConfigurationClick}
|
|
1288
|
+
>
|
|
1289
|
+
<div className="jp-Dialog-buttonLabel">Change provider</div>
|
|
1290
|
+
</button>
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
)}
|
|
1294
|
+
|
|
1295
|
+
{chatEnabled &&
|
|
1296
|
+
(chatMessages.length === 0 ? (
|
|
1297
|
+
<div className="sidebar-messages">
|
|
1298
|
+
<div className="sidebar-greeting">
|
|
1299
|
+
Welcome! How can I assist you today?
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
) : (
|
|
1303
|
+
<div className="sidebar-messages">
|
|
1304
|
+
{chatMessages.map((msg, index) => (
|
|
1305
|
+
<ChatResponse
|
|
1306
|
+
key={`key-${index}`}
|
|
1307
|
+
message={msg}
|
|
1308
|
+
openFile={props.openFile}
|
|
1309
|
+
getApp={props.getApp}
|
|
1310
|
+
getActiveDocumentInfo={props.getActiveDocumentInfo}
|
|
1311
|
+
showGenerating={
|
|
1312
|
+
index === chatMessages.length - 1 &&
|
|
1313
|
+
msg.from === 'copilot' &&
|
|
1314
|
+
copilotRequestInProgress
|
|
1315
|
+
}
|
|
1316
|
+
/>
|
|
1317
|
+
))}
|
|
1318
|
+
<div ref={messagesEndRef} />
|
|
1319
|
+
</div>
|
|
1320
|
+
))}
|
|
1321
|
+
{chatEnabled && (
|
|
1322
|
+
<div
|
|
1323
|
+
className={`sidebar-user-input ${copilotRequestInProgress ? 'generating' : ''}`}
|
|
1324
|
+
>
|
|
1325
|
+
<textarea
|
|
1326
|
+
ref={promptInputRef}
|
|
1327
|
+
rows={3}
|
|
1328
|
+
onChange={onPromptChange}
|
|
1329
|
+
onKeyDown={onPromptKeyDown}
|
|
1330
|
+
placeholder="Ask Copilot..."
|
|
1331
|
+
spellCheck={false}
|
|
1332
|
+
value={prompt}
|
|
1333
|
+
/>
|
|
1334
|
+
{activeDocumentInfo?.filename && (
|
|
1335
|
+
<div className="user-input-context-row">
|
|
1336
|
+
<div
|
|
1337
|
+
className={`user-input-context user-input-context-active-file ${contextOn ? 'on' : 'off'}`}
|
|
1338
|
+
>
|
|
1339
|
+
<div>{currentFileContextTitle}</div>
|
|
1340
|
+
{contextOn ? (
|
|
1341
|
+
<div
|
|
1342
|
+
className="user-input-context-toggle"
|
|
1343
|
+
onClick={() => setContextOn(!contextOn)}
|
|
1344
|
+
>
|
|
1345
|
+
<VscEye title="Use as context" />
|
|
1346
|
+
</div>
|
|
1347
|
+
) : (
|
|
1348
|
+
<div
|
|
1349
|
+
className="user-input-context-toggle"
|
|
1350
|
+
onClick={() => setContextOn(!contextOn)}
|
|
1351
|
+
>
|
|
1352
|
+
<VscEyeClosed title="Don't use as context" />
|
|
1353
|
+
</div>
|
|
1354
|
+
)}
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
)}
|
|
1358
|
+
<div className="user-input-footer">
|
|
1359
|
+
<div>
|
|
1360
|
+
<a
|
|
1361
|
+
href="javascript:void(0)"
|
|
1362
|
+
onClick={() => {
|
|
1363
|
+
setShowPopover(true);
|
|
1364
|
+
promptInputRef.current?.focus();
|
|
1365
|
+
}}
|
|
1366
|
+
title="Select chat participant"
|
|
1367
|
+
>
|
|
1368
|
+
@
|
|
1369
|
+
</a>
|
|
1370
|
+
</div>
|
|
1371
|
+
<div style={{ flexGrow: 1 }}></div>
|
|
1372
|
+
<div>
|
|
1373
|
+
<button
|
|
1374
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled send-button"
|
|
1375
|
+
onClick={() => handleSubmitStopChatButtonClick()}
|
|
1376
|
+
disabled={prompt.length === 0 && !copilotRequestInProgress}
|
|
1377
|
+
>
|
|
1378
|
+
{copilotRequestInProgress ? <VscStopCircle /> : <VscSend />}
|
|
1379
|
+
</button>
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
{showPopover && prefixSuggestions.length > 0 && (
|
|
1383
|
+
<div className="user-input-autocomplete">
|
|
1384
|
+
{prefixSuggestions.map((prefix, index) => (
|
|
1385
|
+
<div
|
|
1386
|
+
key={`key-${index}`}
|
|
1387
|
+
className={`user-input-autocomplete-item ${index === selectedPrefixSuggestionIndex ? 'selected' : ''}`}
|
|
1388
|
+
data-value={prefix}
|
|
1389
|
+
onClick={event => prefixSuggestionSelected(event)}
|
|
1390
|
+
>
|
|
1391
|
+
{prefix}
|
|
1392
|
+
</div>
|
|
1393
|
+
))}
|
|
1394
|
+
</div>
|
|
1395
|
+
)}
|
|
1396
|
+
</div>
|
|
1397
|
+
)}
|
|
1398
|
+
</div>
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function InlinePopoverComponent(props: any) {
|
|
1403
|
+
const [modifiedCode, setModifiedCode] = useState<string>('');
|
|
1404
|
+
const [promptSubmitted, setPromptSubmitted] = useState(false);
|
|
1405
|
+
const originalOnRequestSubmitted = props.onRequestSubmitted;
|
|
1406
|
+
const originalOnResponseEmit = props.onResponseEmit;
|
|
1407
|
+
|
|
1408
|
+
const onRequestSubmitted = (prompt: string) => {
|
|
1409
|
+
setModifiedCode('');
|
|
1410
|
+
setPromptSubmitted(true);
|
|
1411
|
+
originalOnRequestSubmitted(prompt);
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const onResponseEmit = (response: any) => {
|
|
1415
|
+
if (response.type === BackendMessageType.StreamMessage) {
|
|
1416
|
+
const delta = response.data['choices']?.[0]?.['delta'];
|
|
1417
|
+
if (!delta) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const responseMessage =
|
|
1421
|
+
response.data['choices']?.[0]?.['delta']?.['content'];
|
|
1422
|
+
if (!responseMessage) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
setModifiedCode((modifiedCode: string) => modifiedCode + responseMessage);
|
|
1426
|
+
} else if (response.type === BackendMessageType.StreamEnd) {
|
|
1427
|
+
setModifiedCode((modifiedCode: string) =>
|
|
1428
|
+
extractLLMGeneratedCode(modifiedCode)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
originalOnResponseEmit(response);
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
return (
|
|
1436
|
+
<div className="inline-popover">
|
|
1437
|
+
<InlinePromptComponent
|
|
1438
|
+
{...props}
|
|
1439
|
+
onRequestSubmitted={onRequestSubmitted}
|
|
1440
|
+
onResponseEmit={onResponseEmit}
|
|
1441
|
+
onUpdatedCodeAccepted={props.onUpdatedCodeAccepted}
|
|
1442
|
+
limitHeight={props.existingCode !== '' && promptSubmitted}
|
|
1443
|
+
/>
|
|
1444
|
+
{props.existingCode !== '' && promptSubmitted && (
|
|
1445
|
+
<>
|
|
1446
|
+
<InlineDiffViewerComponent {...props} modifiedCode={modifiedCode} />
|
|
1447
|
+
<div className="inline-popover-footer">
|
|
1448
|
+
<div>
|
|
1449
|
+
<button
|
|
1450
|
+
className="jp-Button jp-mod-accept jp-mod-styled jp-mod-small"
|
|
1451
|
+
onClick={() => props.onUpdatedCodeAccepted()}
|
|
1452
|
+
>
|
|
1453
|
+
Accept
|
|
1454
|
+
</button>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div>
|
|
1457
|
+
<button
|
|
1458
|
+
className="jp-Button jp-mod-reject jp-mod-styled jp-mod-small"
|
|
1459
|
+
onClick={() => props.onRequestCancelled()}
|
|
1460
|
+
>
|
|
1461
|
+
Cancel
|
|
1462
|
+
</button>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
</>
|
|
1466
|
+
)}
|
|
1467
|
+
</div>
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function InlineDiffViewerComponent(props: any) {
|
|
1472
|
+
const editorContainerRef = useRef<HTMLDivElement>(null);
|
|
1473
|
+
const [diffEditor, setDiffEditor] =
|
|
1474
|
+
useState<monaco.editor.IStandaloneDiffEditor>(null);
|
|
1475
|
+
|
|
1476
|
+
useEffect(() => {
|
|
1477
|
+
const editorEl = editorContainerRef.current;
|
|
1478
|
+
editorEl.className = 'monaco-editor-container';
|
|
1479
|
+
|
|
1480
|
+
const existingModel = monaco.editor.createModel(
|
|
1481
|
+
props.existingCode,
|
|
1482
|
+
'text/plain'
|
|
1483
|
+
);
|
|
1484
|
+
const modifiedModel = monaco.editor.createModel(
|
|
1485
|
+
props.modifiedCode,
|
|
1486
|
+
'text/plain'
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
const editor = monaco.editor.createDiffEditor(editorEl, {
|
|
1490
|
+
originalEditable: false,
|
|
1491
|
+
automaticLayout: true,
|
|
1492
|
+
theme: isDarkTheme() ? 'vs-dark' : 'vs'
|
|
1493
|
+
});
|
|
1494
|
+
editor.setModel({
|
|
1495
|
+
original: existingModel,
|
|
1496
|
+
modified: modifiedModel
|
|
1497
|
+
});
|
|
1498
|
+
modifiedModel.onDidChangeContent(() => {
|
|
1499
|
+
props.onUpdatedCodeChange(modifiedModel.getValue());
|
|
1500
|
+
});
|
|
1501
|
+
setDiffEditor(editor);
|
|
1502
|
+
}, []);
|
|
1503
|
+
|
|
1504
|
+
useEffect(() => {
|
|
1505
|
+
diffEditor?.getModifiedEditor().getModel()?.setValue(props.modifiedCode);
|
|
1506
|
+
}, [props.modifiedCode]);
|
|
1507
|
+
|
|
1508
|
+
return (
|
|
1509
|
+
<div ref={editorContainerRef} className="monaco-editor-container"></div>
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function InlinePromptComponent(props: any) {
|
|
1514
|
+
const [prompt, setPrompt] = useState<string>(props.prompt);
|
|
1515
|
+
const promptInputRef = useRef<HTMLTextAreaElement>(null);
|
|
1516
|
+
const [inputSubmitted, setInputSubmitted] = useState(false);
|
|
1517
|
+
|
|
1518
|
+
const onPromptChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
1519
|
+
const newPrompt = event.target.value;
|
|
1520
|
+
setPrompt(newPrompt);
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
const handleUserInputSubmit = async () => {
|
|
1524
|
+
const promptPrefixParts = [];
|
|
1525
|
+
const promptParts = prompt.split(' ');
|
|
1526
|
+
if (promptParts.length > 1) {
|
|
1527
|
+
for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
|
|
1528
|
+
const part = promptParts[i];
|
|
1529
|
+
if (part.startsWith('@') || part.startsWith('/')) {
|
|
1530
|
+
promptPrefixParts.push(part);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
submitCompletionRequest(
|
|
1536
|
+
{
|
|
1537
|
+
messageId: UUID.uuid4(),
|
|
1538
|
+
chatId: UUID.uuid4(),
|
|
1539
|
+
type: RunChatCompletionType.GenerateCode,
|
|
1540
|
+
content: prompt,
|
|
1541
|
+
language: undefined,
|
|
1542
|
+
filename: undefined,
|
|
1543
|
+
prefix: props.prefix,
|
|
1544
|
+
suffix: props.suffix,
|
|
1545
|
+
existingCode: props.existingCode
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
emit: async response => {
|
|
1549
|
+
props.onResponseEmit(response);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
setInputSubmitted(true);
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
const onPromptKeyDown = async (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
1558
|
+
if (event.key === 'Enter') {
|
|
1559
|
+
event.stopPropagation();
|
|
1560
|
+
event.preventDefault();
|
|
1561
|
+
if (inputSubmitted && (event.metaKey || event.ctrlKey)) {
|
|
1562
|
+
props.onUpdatedCodeAccepted();
|
|
1563
|
+
} else {
|
|
1564
|
+
props.onRequestSubmitted(prompt);
|
|
1565
|
+
handleUserInputSubmit();
|
|
1566
|
+
}
|
|
1567
|
+
} else if (event.key === 'Escape') {
|
|
1568
|
+
event.stopPropagation();
|
|
1569
|
+
event.preventDefault();
|
|
1570
|
+
props.onRequestCancelled();
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
useEffect(() => {
|
|
1575
|
+
const input = promptInputRef.current;
|
|
1576
|
+
if (input) {
|
|
1577
|
+
input.select();
|
|
1578
|
+
promptInputRef.current?.focus();
|
|
1579
|
+
}
|
|
1580
|
+
}, []);
|
|
1581
|
+
|
|
1582
|
+
return (
|
|
1583
|
+
<div
|
|
1584
|
+
className="inline-prompt-container"
|
|
1585
|
+
style={{ height: props.limitHeight ? '40px' : '100%' }}
|
|
1586
|
+
>
|
|
1587
|
+
<textarea
|
|
1588
|
+
ref={promptInputRef}
|
|
1589
|
+
rows={3}
|
|
1590
|
+
onChange={onPromptChange}
|
|
1591
|
+
onKeyDown={onPromptKeyDown}
|
|
1592
|
+
placeholder="Ask Copilot to generate Python code..."
|
|
1593
|
+
spellCheck={false}
|
|
1594
|
+
value={prompt}
|
|
1595
|
+
/>
|
|
1596
|
+
</div>
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function GitHubCopilotStatusComponent(props: any) {
|
|
1601
|
+
const [ghLoginStatus, setGHLoginStatus] = useState(
|
|
1602
|
+
GitHubCopilotLoginStatus.NotLoggedIn
|
|
1603
|
+
);
|
|
1604
|
+
const [loginClickCount, _setLoginClickCount] = useState(0);
|
|
1605
|
+
|
|
1606
|
+
useEffect(() => {
|
|
1607
|
+
const fetchData = () => {
|
|
1608
|
+
setGHLoginStatus(NBIAPI.getLoginStatus());
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
fetchData();
|
|
1612
|
+
|
|
1613
|
+
const intervalId = setInterval(fetchData, 1000);
|
|
1614
|
+
|
|
1615
|
+
return () => clearInterval(intervalId);
|
|
1616
|
+
}, [loginClickCount]);
|
|
1617
|
+
|
|
1618
|
+
const onStatusClick = () => {
|
|
1619
|
+
props
|
|
1620
|
+
.getApp()
|
|
1621
|
+
.commands.execute(
|
|
1622
|
+
'notebook-intelligence:open-github-copilot-login-dialog'
|
|
1623
|
+
);
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
return (
|
|
1627
|
+
<div
|
|
1628
|
+
title={`GitHub Copilot: ${ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn ? 'Logged in' : 'Not logged in'}`}
|
|
1629
|
+
className="github-copilot-status-bar"
|
|
1630
|
+
onClick={() => onStatusClick()}
|
|
1631
|
+
dangerouslySetInnerHTML={{
|
|
1632
|
+
__html:
|
|
1633
|
+
ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn
|
|
1634
|
+
? copilotSvgstr
|
|
1635
|
+
: copilotWarningSvgstr
|
|
1636
|
+
}}
|
|
1637
|
+
></div>
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function GitHubCopilotLoginDialogBodyComponent(props: any) {
|
|
1642
|
+
const [ghLoginStatus, setGHLoginStatus] = useState(
|
|
1643
|
+
GitHubCopilotLoginStatus.NotLoggedIn
|
|
1644
|
+
);
|
|
1645
|
+
const [loginClickCount, setLoginClickCount] = useState(0);
|
|
1646
|
+
const [loginClicked, setLoginClicked] = useState(false);
|
|
1647
|
+
const [deviceActivationURL, setDeviceActivationURL] = useState('');
|
|
1648
|
+
const [deviceActivationCode, setDeviceActivationCode] = useState('');
|
|
1649
|
+
|
|
1650
|
+
useEffect(() => {
|
|
1651
|
+
const fetchData = () => {
|
|
1652
|
+
const status = NBIAPI.getLoginStatus();
|
|
1653
|
+
setGHLoginStatus(status);
|
|
1654
|
+
if (status === GitHubCopilotLoginStatus.LoggedIn && loginClicked) {
|
|
1655
|
+
setTimeout(() => {
|
|
1656
|
+
props.onLoggedIn();
|
|
1657
|
+
}, 1000);
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
fetchData();
|
|
1662
|
+
|
|
1663
|
+
const intervalId = setInterval(fetchData, 1000);
|
|
1664
|
+
|
|
1665
|
+
return () => clearInterval(intervalId);
|
|
1666
|
+
}, [loginClickCount]);
|
|
1667
|
+
|
|
1668
|
+
const handleLoginClick = async () => {
|
|
1669
|
+
const response = await NBIAPI.loginToGitHub();
|
|
1670
|
+
setDeviceActivationURL((response as any).verificationURI);
|
|
1671
|
+
setDeviceActivationCode((response as any).userCode);
|
|
1672
|
+
setLoginClickCount(loginClickCount + 1);
|
|
1673
|
+
setLoginClicked(true);
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const handleLogoutClick = async () => {
|
|
1677
|
+
await NBIAPI.logoutFromGitHub();
|
|
1678
|
+
setLoginClickCount(loginClickCount + 1);
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
const loggedIn = ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn;
|
|
1682
|
+
|
|
1683
|
+
return (
|
|
1684
|
+
<div className="github-copilot-login-dialog">
|
|
1685
|
+
<div className="github-copilot-login-status">
|
|
1686
|
+
<h4>
|
|
1687
|
+
Login status:{' '}
|
|
1688
|
+
<span
|
|
1689
|
+
className={`github-copilot-login-status-text ${loggedIn ? 'logged-in' : ''}`}
|
|
1690
|
+
>
|
|
1691
|
+
{loggedIn
|
|
1692
|
+
? 'Logged in'
|
|
1693
|
+
: ghLoginStatus === GitHubCopilotLoginStatus.LoggingIn
|
|
1694
|
+
? 'Logging in...'
|
|
1695
|
+
: ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice
|
|
1696
|
+
? 'Activating device...'
|
|
1697
|
+
: ghLoginStatus === GitHubCopilotLoginStatus.NotLoggedIn
|
|
1698
|
+
? 'Not logged in'
|
|
1699
|
+
: 'Unknown'}
|
|
1700
|
+
</span>
|
|
1701
|
+
</h4>
|
|
1702
|
+
</div>
|
|
1703
|
+
|
|
1704
|
+
{ghLoginStatus === GitHubCopilotLoginStatus.NotLoggedIn && (
|
|
1705
|
+
<>
|
|
1706
|
+
<div>
|
|
1707
|
+
Your code and data are directly transferred to GitHub Copilot as
|
|
1708
|
+
needed without storing any copies other than keeping in the process
|
|
1709
|
+
memory.
|
|
1710
|
+
</div>
|
|
1711
|
+
<div>
|
|
1712
|
+
<a href="https://github.com/features/copilot" target="_blank">
|
|
1713
|
+
GitHub Copilot
|
|
1714
|
+
</a>{' '}
|
|
1715
|
+
requires a subscription and it has a free tier. GitHub Copilot is
|
|
1716
|
+
subject to the{' '}
|
|
1717
|
+
<a
|
|
1718
|
+
href="https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features"
|
|
1719
|
+
target="_blank"
|
|
1720
|
+
>
|
|
1721
|
+
GitHub Terms for Additional Products and Features
|
|
1722
|
+
</a>
|
|
1723
|
+
.
|
|
1724
|
+
</div>
|
|
1725
|
+
<div>
|
|
1726
|
+
<h4>Privacy and terms</h4>
|
|
1727
|
+
By using Notebook Intelligence with GitHub Copilot subscription you
|
|
1728
|
+
agree to{' '}
|
|
1729
|
+
<a
|
|
1730
|
+
href="https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-your-ide"
|
|
1731
|
+
target="_blank"
|
|
1732
|
+
>
|
|
1733
|
+
GitHub Copilot chat terms
|
|
1734
|
+
</a>
|
|
1735
|
+
. Review the terms to understand about usage, limitations and ways
|
|
1736
|
+
to improve GitHub Copilot. Please review{' '}
|
|
1737
|
+
<a
|
|
1738
|
+
href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
|
|
1739
|
+
target="_blank"
|
|
1740
|
+
>
|
|
1741
|
+
Privacy Statement
|
|
1742
|
+
</a>
|
|
1743
|
+
.
|
|
1744
|
+
</div>
|
|
1745
|
+
<div>
|
|
1746
|
+
<button
|
|
1747
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-reject jp-mod-styled"
|
|
1748
|
+
onClick={handleLoginClick}
|
|
1749
|
+
>
|
|
1750
|
+
<div className="jp-Dialog-buttonLabel">
|
|
1751
|
+
Login using your GitHub account
|
|
1752
|
+
</div>
|
|
1753
|
+
</button>
|
|
1754
|
+
</div>
|
|
1755
|
+
</>
|
|
1756
|
+
)}
|
|
1757
|
+
|
|
1758
|
+
{loggedIn && (
|
|
1759
|
+
<div>
|
|
1760
|
+
<button
|
|
1761
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
1762
|
+
onClick={handleLogoutClick}
|
|
1763
|
+
>
|
|
1764
|
+
<div className="jp-Dialog-buttonLabel">Logout</div>
|
|
1765
|
+
</button>
|
|
1766
|
+
</div>
|
|
1767
|
+
)}
|
|
1768
|
+
|
|
1769
|
+
{ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice &&
|
|
1770
|
+
deviceActivationURL &&
|
|
1771
|
+
deviceActivationCode && (
|
|
1772
|
+
<div>
|
|
1773
|
+
<div className="copilot-activation-message">
|
|
1774
|
+
Copy code{' '}
|
|
1775
|
+
<span
|
|
1776
|
+
className="user-code-span"
|
|
1777
|
+
onClick={() => {
|
|
1778
|
+
navigator.clipboard.writeText(deviceActivationCode);
|
|
1779
|
+
return true;
|
|
1780
|
+
}}
|
|
1781
|
+
>
|
|
1782
|
+
<b>
|
|
1783
|
+
{deviceActivationCode}{' '}
|
|
1784
|
+
<span
|
|
1785
|
+
className="copy-icon"
|
|
1786
|
+
dangerouslySetInnerHTML={{ __html: copySvgstr }}
|
|
1787
|
+
></span>
|
|
1788
|
+
</b>
|
|
1789
|
+
</span>{' '}
|
|
1790
|
+
and enter at{' '}
|
|
1791
|
+
<a href={deviceActivationURL} target="_blank">
|
|
1792
|
+
{deviceActivationURL}
|
|
1793
|
+
</a>{' '}
|
|
1794
|
+
to allow access to GitHub Copilot from this app. Activation could
|
|
1795
|
+
take up to a minute after you enter the code.
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
)}
|
|
1799
|
+
|
|
1800
|
+
{ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice && (
|
|
1801
|
+
<div style={{ marginTop: '10px' }}>
|
|
1802
|
+
<button
|
|
1803
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
1804
|
+
onClick={handleLogoutClick}
|
|
1805
|
+
>
|
|
1806
|
+
<div className="jp-Dialog-buttonLabel">Cancel activation</div>
|
|
1807
|
+
</button>
|
|
1808
|
+
</div>
|
|
1809
|
+
)}
|
|
1810
|
+
</div>
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function ConfigurationDialogBodyComponent(props: any) {
|
|
1815
|
+
const nbiConfig = NBIAPI.config;
|
|
1816
|
+
const llmProviders = nbiConfig.llmProviders;
|
|
1817
|
+
const [chatModels, setChatModels] = useState([]);
|
|
1818
|
+
const [inlineCompletionModels, setInlineCompletionModels] = useState([]);
|
|
1819
|
+
|
|
1820
|
+
const handleSaveClick = async () => {
|
|
1821
|
+
await NBIAPI.setConfig({
|
|
1822
|
+
chat_model: {
|
|
1823
|
+
provider: chatModelProvider,
|
|
1824
|
+
model: chatModel,
|
|
1825
|
+
properties: chatModelProperties
|
|
1826
|
+
},
|
|
1827
|
+
inline_completion_model: {
|
|
1828
|
+
provider: inlineCompletionModelProvider,
|
|
1829
|
+
model: inlineCompletionModel,
|
|
1830
|
+
properties: inlineCompletionModelProperties
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
props.onSave();
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const handleRefreshOllamaModelListClick = async () => {
|
|
1838
|
+
await NBIAPI.updateOllamaModelList();
|
|
1839
|
+
updateModelOptionsForProvider(chatModelProvider, 'chat');
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
const [chatModelProvider, setChatModelProvider] = useState(
|
|
1843
|
+
nbiConfig.chatModel.provider || 'none'
|
|
1844
|
+
);
|
|
1845
|
+
const [inlineCompletionModelProvider, setInlineCompletionModelProvider] =
|
|
1846
|
+
useState(nbiConfig.inlineCompletionModel.provider || 'none');
|
|
1847
|
+
const [chatModel, setChatModel] = useState<string>(nbiConfig.chatModel.model);
|
|
1848
|
+
const [chatModelProperties, setChatModelProperties] = useState<any[]>([]);
|
|
1849
|
+
const [inlineCompletionModelProperties, setInlineCompletionModelProperties] =
|
|
1850
|
+
useState<any[]>([]);
|
|
1851
|
+
const [inlineCompletionModel, setInlineCompletionModel] = useState(
|
|
1852
|
+
nbiConfig.inlineCompletionModel.model
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
const updateModelOptionsForProvider = (
|
|
1856
|
+
providerId: string,
|
|
1857
|
+
modelType: 'chat' | 'inline-completion'
|
|
1858
|
+
) => {
|
|
1859
|
+
if (modelType === 'chat') {
|
|
1860
|
+
setChatModelProvider(providerId);
|
|
1861
|
+
} else {
|
|
1862
|
+
setInlineCompletionModelProvider(providerId);
|
|
1863
|
+
}
|
|
1864
|
+
const models =
|
|
1865
|
+
modelType === 'chat'
|
|
1866
|
+
? nbiConfig.chatModels
|
|
1867
|
+
: nbiConfig.inlineCompletionModels;
|
|
1868
|
+
const selectedModelId =
|
|
1869
|
+
modelType === 'chat'
|
|
1870
|
+
? nbiConfig.chatModel.model
|
|
1871
|
+
: nbiConfig.inlineCompletionModel.model;
|
|
1872
|
+
|
|
1873
|
+
const providerModels = models.filter(
|
|
1874
|
+
(model: any) => model.provider === providerId
|
|
1875
|
+
);
|
|
1876
|
+
if (modelType === 'chat') {
|
|
1877
|
+
setChatModels(providerModels);
|
|
1878
|
+
} else {
|
|
1879
|
+
setInlineCompletionModels(providerModels);
|
|
1880
|
+
}
|
|
1881
|
+
let selectedModel = providerModels.find(
|
|
1882
|
+
(model: any) => model.id === selectedModelId
|
|
1883
|
+
);
|
|
1884
|
+
if (!selectedModel) {
|
|
1885
|
+
selectedModel = providerModels?.[0];
|
|
1886
|
+
}
|
|
1887
|
+
if (selectedModel) {
|
|
1888
|
+
if (modelType === 'chat') {
|
|
1889
|
+
setChatModel(selectedModel.id);
|
|
1890
|
+
setChatModelProperties(selectedModel.properties);
|
|
1891
|
+
} else {
|
|
1892
|
+
setInlineCompletionModel(selectedModel.id);
|
|
1893
|
+
setInlineCompletionModelProperties(selectedModel.properties);
|
|
1894
|
+
}
|
|
1895
|
+
} else {
|
|
1896
|
+
if (modelType === 'chat') {
|
|
1897
|
+
setChatModelProperties([]);
|
|
1898
|
+
} else {
|
|
1899
|
+
setInlineCompletionModelProperties([]);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
const onModelPropertyChange = (
|
|
1905
|
+
modelType: 'chat' | 'inline-completion',
|
|
1906
|
+
propertyId: string,
|
|
1907
|
+
value: string
|
|
1908
|
+
) => {
|
|
1909
|
+
const modelProperties =
|
|
1910
|
+
modelType === 'chat'
|
|
1911
|
+
? chatModelProperties
|
|
1912
|
+
: inlineCompletionModelProperties;
|
|
1913
|
+
const updatedProperties = modelProperties.map((property: any) => {
|
|
1914
|
+
if (property.id === propertyId) {
|
|
1915
|
+
return { ...property, value };
|
|
1916
|
+
}
|
|
1917
|
+
return property;
|
|
1918
|
+
});
|
|
1919
|
+
if (modelType === 'chat') {
|
|
1920
|
+
setChatModelProperties(updatedProperties);
|
|
1921
|
+
} else {
|
|
1922
|
+
setInlineCompletionModelProperties(updatedProperties);
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
useEffect(() => {
|
|
1927
|
+
updateModelOptionsForProvider(chatModelProvider, 'chat');
|
|
1928
|
+
updateModelOptionsForProvider(
|
|
1929
|
+
inlineCompletionModelProvider,
|
|
1930
|
+
'inline-completion'
|
|
1931
|
+
);
|
|
1932
|
+
}, []);
|
|
1933
|
+
|
|
1934
|
+
return (
|
|
1935
|
+
<div className="config-dialog">
|
|
1936
|
+
<div className="config-dialog-body">
|
|
1937
|
+
<div className="model-config-section">
|
|
1938
|
+
<div className="model-config-section-header">Chat model</div>
|
|
1939
|
+
<div className="model-config-section-body">
|
|
1940
|
+
<div className="model-config-section-row">
|
|
1941
|
+
<div className="model-config-section-column">
|
|
1942
|
+
<div>Provider</div>
|
|
1943
|
+
<div>
|
|
1944
|
+
<select
|
|
1945
|
+
className="jp-mod-styled"
|
|
1946
|
+
onChange={event =>
|
|
1947
|
+
updateModelOptionsForProvider(event.target.value, 'chat')
|
|
1948
|
+
}
|
|
1949
|
+
>
|
|
1950
|
+
{llmProviders.map((provider: any, index: number) => (
|
|
1951
|
+
<option
|
|
1952
|
+
key={index}
|
|
1953
|
+
value={provider.id}
|
|
1954
|
+
selected={provider.id === chatModelProvider}
|
|
1955
|
+
>
|
|
1956
|
+
{provider.name}
|
|
1957
|
+
</option>
|
|
1958
|
+
))}
|
|
1959
|
+
<option
|
|
1960
|
+
key={-1}
|
|
1961
|
+
value="none"
|
|
1962
|
+
selected={
|
|
1963
|
+
chatModelProvider === 'none' ||
|
|
1964
|
+
!llmProviders.find(
|
|
1965
|
+
provider => provider.id === chatModelProvider
|
|
1966
|
+
)
|
|
1967
|
+
}
|
|
1968
|
+
>
|
|
1969
|
+
None
|
|
1970
|
+
</option>
|
|
1971
|
+
</select>
|
|
1972
|
+
</div>
|
|
1973
|
+
</div>
|
|
1974
|
+
{!['openai-compatible', 'litellm-compatible', 'none'].includes(
|
|
1975
|
+
chatModelProvider
|
|
1976
|
+
) &&
|
|
1977
|
+
chatModels.length > 0 && (
|
|
1978
|
+
<div className="model-config-section-column">
|
|
1979
|
+
<div>Model</div>
|
|
1980
|
+
{![
|
|
1981
|
+
OPENAI_COMPATIBLE_CHAT_MODEL_ID,
|
|
1982
|
+
LITELLM_COMPATIBLE_CHAT_MODEL_ID
|
|
1983
|
+
].includes(chatModel) &&
|
|
1984
|
+
chatModels.length > 0 && (
|
|
1985
|
+
<div>
|
|
1986
|
+
<select
|
|
1987
|
+
className="jp-mod-styled"
|
|
1988
|
+
onChange={event => setChatModel(event.target.value)}
|
|
1989
|
+
>
|
|
1990
|
+
{chatModels.map((model: any, index: number) => (
|
|
1991
|
+
<option
|
|
1992
|
+
key={index}
|
|
1993
|
+
value={model.id}
|
|
1994
|
+
selected={model.id === chatModel}
|
|
1995
|
+
>
|
|
1996
|
+
{model.name}
|
|
1997
|
+
</option>
|
|
1998
|
+
))}
|
|
1999
|
+
</select>
|
|
2000
|
+
</div>
|
|
2001
|
+
)}
|
|
2002
|
+
</div>
|
|
2003
|
+
)}
|
|
2004
|
+
</div>
|
|
2005
|
+
|
|
2006
|
+
<div className="model-config-section-row">
|
|
2007
|
+
<div className="model-config-section-column">
|
|
2008
|
+
{chatModelProvider === 'ollama' && chatModels.length === 0 && (
|
|
2009
|
+
<div className="ollama-warning-message">
|
|
2010
|
+
No Ollama models found! Make sure{' '}
|
|
2011
|
+
<a href="https://ollama.com/" target="_blank">
|
|
2012
|
+
Ollama
|
|
2013
|
+
</a>{' '}
|
|
2014
|
+
is running and models are downloaded to your computer.{' '}
|
|
2015
|
+
<a
|
|
2016
|
+
href="javascript:void(0)"
|
|
2017
|
+
onClick={handleRefreshOllamaModelListClick}
|
|
2018
|
+
>
|
|
2019
|
+
Try again
|
|
2020
|
+
</a>{' '}
|
|
2021
|
+
once ready.
|
|
2022
|
+
</div>
|
|
2023
|
+
)}
|
|
2024
|
+
</div>
|
|
2025
|
+
</div>
|
|
2026
|
+
|
|
2027
|
+
<div className="model-config-section-row">
|
|
2028
|
+
<div className="model-config-section-column">
|
|
2029
|
+
{chatModelProperties.map((property: any, index: number) => (
|
|
2030
|
+
<div className="form-field-row" key={index}>
|
|
2031
|
+
<div className="form-field-description">
|
|
2032
|
+
{property.name} {property.optional ? '(optional)' : ''}
|
|
2033
|
+
</div>
|
|
2034
|
+
<input
|
|
2035
|
+
name="chat-model-id-input"
|
|
2036
|
+
placeholder={property.description}
|
|
2037
|
+
className="jp-mod-styled"
|
|
2038
|
+
spellCheck={false}
|
|
2039
|
+
value={property.value}
|
|
2040
|
+
onChange={event =>
|
|
2041
|
+
onModelPropertyChange(
|
|
2042
|
+
'chat',
|
|
2043
|
+
property.id,
|
|
2044
|
+
event.target.value
|
|
2045
|
+
)
|
|
2046
|
+
}
|
|
2047
|
+
/>
|
|
2048
|
+
</div>
|
|
2049
|
+
))}
|
|
2050
|
+
</div>
|
|
2051
|
+
</div>
|
|
2052
|
+
</div>
|
|
2053
|
+
</div>
|
|
2054
|
+
|
|
2055
|
+
<div className="model-config-section">
|
|
2056
|
+
<div className="model-config-section-header">Auto-complete model</div>
|
|
2057
|
+
<div className="model-config-section-body">
|
|
2058
|
+
<div className="model-config-section-row">
|
|
2059
|
+
<div className="model-config-section-column">
|
|
2060
|
+
<div>Provider</div>
|
|
2061
|
+
<div>
|
|
2062
|
+
<select
|
|
2063
|
+
className="jp-mod-styled"
|
|
2064
|
+
onChange={event =>
|
|
2065
|
+
updateModelOptionsForProvider(
|
|
2066
|
+
event.target.value,
|
|
2067
|
+
'inline-completion'
|
|
2068
|
+
)
|
|
2069
|
+
}
|
|
2070
|
+
>
|
|
2071
|
+
{llmProviders.map((provider: any, index: number) => (
|
|
2072
|
+
<option
|
|
2073
|
+
key={index}
|
|
2074
|
+
value={provider.id}
|
|
2075
|
+
selected={provider.id === inlineCompletionModelProvider}
|
|
2076
|
+
>
|
|
2077
|
+
{provider.name}
|
|
2078
|
+
</option>
|
|
2079
|
+
))}
|
|
2080
|
+
<option
|
|
2081
|
+
key={-1}
|
|
2082
|
+
value="none"
|
|
2083
|
+
selected={
|
|
2084
|
+
inlineCompletionModelProvider === 'none' ||
|
|
2085
|
+
!llmProviders.find(
|
|
2086
|
+
provider =>
|
|
2087
|
+
provider.id === inlineCompletionModelProvider
|
|
2088
|
+
)
|
|
2089
|
+
}
|
|
2090
|
+
>
|
|
2091
|
+
None
|
|
2092
|
+
</option>
|
|
2093
|
+
</select>
|
|
2094
|
+
</div>
|
|
2095
|
+
</div>
|
|
2096
|
+
{!['openai-compatible', 'litellm-compatible', 'none'].includes(
|
|
2097
|
+
inlineCompletionModelProvider
|
|
2098
|
+
) && (
|
|
2099
|
+
<div className="model-config-section-column">
|
|
2100
|
+
<div>Model</div>
|
|
2101
|
+
{![
|
|
2102
|
+
OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID,
|
|
2103
|
+
LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID
|
|
2104
|
+
].includes(inlineCompletionModel) && (
|
|
2105
|
+
<div>
|
|
2106
|
+
<select
|
|
2107
|
+
className="jp-mod-styled"
|
|
2108
|
+
onChange={event =>
|
|
2109
|
+
setInlineCompletionModel(event.target.value)
|
|
2110
|
+
}
|
|
2111
|
+
>
|
|
2112
|
+
{inlineCompletionModels.map(
|
|
2113
|
+
(model: any, index: number) => (
|
|
2114
|
+
<option
|
|
2115
|
+
key={index}
|
|
2116
|
+
value={model.id}
|
|
2117
|
+
selected={model.id === inlineCompletionModel}
|
|
2118
|
+
>
|
|
2119
|
+
{model.name}
|
|
2120
|
+
</option>
|
|
2121
|
+
)
|
|
2122
|
+
)}
|
|
2123
|
+
</select>
|
|
2124
|
+
</div>
|
|
2125
|
+
)}
|
|
2126
|
+
</div>
|
|
2127
|
+
)}
|
|
2128
|
+
</div>
|
|
2129
|
+
|
|
2130
|
+
<div className="model-config-section-row">
|
|
2131
|
+
<div className="model-config-section-column">
|
|
2132
|
+
{inlineCompletionModelProperties.map(
|
|
2133
|
+
(property: any, index: number) => (
|
|
2134
|
+
<div className="form-field-row" key={index}>
|
|
2135
|
+
<div className="form-field-description">
|
|
2136
|
+
{property.name} {property.optional ? '(optional)' : ''}
|
|
2137
|
+
</div>
|
|
2138
|
+
<input
|
|
2139
|
+
name="inline-completion-model-id-input"
|
|
2140
|
+
placeholder={property.description}
|
|
2141
|
+
className="jp-mod-styled"
|
|
2142
|
+
spellCheck={false}
|
|
2143
|
+
value={property.value}
|
|
2144
|
+
onChange={event =>
|
|
2145
|
+
onModelPropertyChange(
|
|
2146
|
+
'inline-completion',
|
|
2147
|
+
property.id,
|
|
2148
|
+
event.target.value
|
|
2149
|
+
)
|
|
2150
|
+
}
|
|
2151
|
+
/>
|
|
2152
|
+
</div>
|
|
2153
|
+
)
|
|
2154
|
+
)}
|
|
2155
|
+
</div>
|
|
2156
|
+
</div>
|
|
2157
|
+
</div>
|
|
2158
|
+
</div>
|
|
2159
|
+
</div>
|
|
2160
|
+
|
|
2161
|
+
<div className="config-dialog-footer">
|
|
2162
|
+
<button
|
|
2163
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
2164
|
+
onClick={handleSaveClick}
|
|
2165
|
+
>
|
|
2166
|
+
<div className="jp-Dialog-buttonLabel">Save</div>
|
|
2167
|
+
</button>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
);
|
|
2171
|
+
}
|