@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.
@@ -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}`}>&#x2713; {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>&#10006; 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
+ }