@notebook-intelligence/notebook-intelligence
Version:
AI coding assistant for JupyterLab
1,056 lines • 94.7 kB
JavaScript
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
import { ReactWidget } from '@jupyterlab/apputils';
import { UUID } from '@lumino/coreutils';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
import { NBIAPI, GitHubCopilotLoginStatus } from './api';
import { BackendMessageType, BuiltinToolsetType, ContextType, GITHUB_COPILOT_PROVIDER_ID, RequestDataType, ResponseStreamDataType, TelemetryEventType } from './tokens';
import { MarkdownRenderer as OriginalMarkdownRenderer } from './markdown-renderer';
const MarkdownRenderer = memo(OriginalMarkdownRenderer);
import copySvgstr from '../style/icons/copy.svg';
import copilotSvgstr from '../style/icons/copilot.svg';
import copilotWarningSvgstr from '../style/icons/copilot-warning.svg';
import { VscSend, VscStopCircle, VscEye, VscEyeClosed, VscTriangleRight, VscTriangleDown, VscWarning, VscSettingsGear, VscPassFilled, VscTools, VscTrash } from 'react-icons/vsc';
import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from 'react-icons/md';
import { extractLLMGeneratedCode, isDarkTheme } from './utils';
const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
const OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID = 'openai-compatible-inline-completion-model';
const LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID = 'litellm-compatible-inline-completion-model';
export var RunChatCompletionType;
(function (RunChatCompletionType) {
RunChatCompletionType[RunChatCompletionType["Chat"] = 0] = "Chat";
RunChatCompletionType[RunChatCompletionType["ExplainThis"] = 1] = "ExplainThis";
RunChatCompletionType[RunChatCompletionType["FixThis"] = 2] = "FixThis";
RunChatCompletionType[RunChatCompletionType["GenerateCode"] = 3] = "GenerateCode";
RunChatCompletionType[RunChatCompletionType["ExplainThisOutput"] = 4] = "ExplainThisOutput";
RunChatCompletionType[RunChatCompletionType["TroubleshootThisOutput"] = 5] = "TroubleshootThisOutput";
})(RunChatCompletionType || (RunChatCompletionType = {}));
export class ChatSidebar extends ReactWidget {
constructor(options) {
super();
this._options = options;
this.node.style.height = '100%';
}
render() {
return (React.createElement(SidebarComponent, { getActiveDocumentInfo: this._options.getActiveDocumentInfo, getActiveSelectionContent: this._options.getActiveSelectionContent, getCurrentCellContents: this._options.getCurrentCellContents, openFile: this._options.openFile, getApp: this._options.getApp, getTelemetryEmitter: this._options.getTelemetryEmitter }));
}
}
export class InlinePromptWidget extends ReactWidget {
constructor(rect, options) {
super();
this.node.classList.add('inline-prompt-widget');
this.node.style.top = `${rect.top + 32}px`;
this.node.style.left = `${rect.left}px`;
this.node.style.width = rect.width + 'px';
this.node.style.height = '48px';
this._options = options;
this.node.addEventListener('focusout', (event) => {
if (this.node.contains(event.relatedTarget)) {
return;
}
this._options.onRequestCancelled();
});
}
updatePosition(rect) {
this.node.style.top = `${rect.top + 32}px`;
this.node.style.left = `${rect.left}px`;
this.node.style.width = rect.width + 'px';
}
_onResponse(response) {
var _a, _b, _c, _d, _e;
if (response.type === BackendMessageType.StreamMessage) {
const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
if (!delta) {
return;
}
const responseMessage = (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
if (!responseMessage) {
return;
}
this._options.onContentStream(responseMessage);
}
else if (response.type === BackendMessageType.StreamEnd) {
this._options.onContentStreamEnd();
const timeElapsed = (new Date().getTime() - this._requestTime.getTime()) / 1000;
this._options.telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.InlineChatResponse,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
timeElapsed
}
});
}
}
_onRequestSubmitted(prompt) {
// code update
if (this._options.existingCode !== '') {
this.node.style.height = '300px';
}
// save the prompt in case of a rerender
this._options.prompt = prompt;
this._options.onRequestSubmitted(prompt);
this._requestTime = new Date();
this._options.telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.InlineChatRequest,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
prompt: prompt
}
});
}
render() {
return (React.createElement(InlinePopoverComponent, { prompt: this._options.prompt, existingCode: this._options.existingCode, onRequestSubmitted: this._onRequestSubmitted.bind(this), onRequestCancelled: this._options.onRequestCancelled, onResponseEmit: this._onResponse.bind(this), prefix: this._options.prefix, suffix: this._options.suffix, onUpdatedCodeChange: this._options.onUpdatedCodeChange, onUpdatedCodeAccepted: this._options.onUpdatedCodeAccepted }));
}
}
export class GitHubCopilotStatusBarItem extends ReactWidget {
constructor(options) {
super();
this._getApp = options.getApp;
}
render() {
return React.createElement(GitHubCopilotStatusComponent, { getApp: this._getApp });
}
}
export class GitHubCopilotLoginDialogBody extends ReactWidget {
constructor(options) {
super();
this._onLoggedIn = options.onLoggedIn;
}
render() {
return (React.createElement(GitHubCopilotLoginDialogBodyComponent, { onLoggedIn: () => this._onLoggedIn() }));
}
}
export class ConfigurationDialogBody extends ReactWidget {
constructor(options) {
super();
this._onSave = options.onSave;
}
render() {
return React.createElement(ConfigurationDialogBodyComponent, { onSave: this._onSave });
}
}
const answeredForms = new Map();
function ChatResponseHTMLFrame(props) {
const iframSrc = useMemo(() => URL.createObjectURL(new Blob([props.source], { type: 'text/html' })), []);
return (React.createElement("div", { className: "chat-response-html-frame", key: `key-${props.index}` },
React.createElement("iframe", { className: "chat-response-html-frame-iframe", height: props.height, sandbox: "allow-scripts", src: iframSrc })));
}
// Memoize ChatResponse for performance
function ChatResponse(props) {
var _a, _b, _c;
const [renderCount, setRenderCount] = useState(0);
const msg = props.message;
const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
const openNotebook = (event) => {
const notebookPath = event.target.dataset['ref'];
props.openFile(notebookPath);
};
const markFormConfirmed = (contentId) => {
answeredForms.set(contentId, 'confirmed');
setRenderCount(prev => prev + 1);
};
const markFormCanceled = (contentId) => {
answeredForms.set(contentId, 'canceled');
setRenderCount(prev => prev + 1);
};
const runCommand = (commandId, args) => {
props.getApp().commands.execute(commandId, args);
};
// group messages by type
const groupedContents = [];
let lastItemType;
const extractReasoningContent = (item) => {
let currentContent = item.content;
if (typeof currentContent !== 'string') {
return false;
}
let reasoningContent = '';
let reasoningStartTime = new Date();
const reasoningEndTime = new Date();
const startPos = currentContent.indexOf('<think>');
const hasStart = startPos >= 0;
reasoningStartTime = new Date(item.created);
if (hasStart) {
currentContent = currentContent.substring(startPos + 7);
}
const endPos = currentContent.indexOf('</think>');
const hasEnd = endPos >= 0;
if (hasEnd) {
reasoningContent += currentContent.substring(0, endPos);
currentContent = currentContent.substring(endPos + 8);
}
else {
if (hasStart) {
reasoningContent += currentContent;
currentContent = '';
}
}
item.content = currentContent;
item.reasoningContent = reasoningContent;
item.reasoningFinished = hasEnd;
item.reasoningTime =
(reasoningEndTime.getTime() - reasoningStartTime.getTime()) / 1000;
return hasStart && !hasEnd; // is thinking
};
for (let i = 0; i < msg.contents.length; i++) {
const item = msg.contents[i];
if (item.type === lastItemType &&
lastItemType === ResponseStreamDataType.MarkdownPart) {
const lastItem = groupedContents[groupedContents.length - 1];
lastItem.content += item.content;
}
else {
groupedContents.push(structuredClone(item));
lastItemType = item.type;
}
}
const [thinkingInProgress, setThinkingInProgress] = useState(false);
for (const item of groupedContents) {
const isThinking = extractReasoningContent(item);
if (isThinking && !thinkingInProgress) {
setThinkingInProgress(true);
}
}
useEffect(() => {
let intervalId = undefined;
if (thinkingInProgress) {
intervalId = setInterval(() => {
setRenderCount(prev => prev + 1);
setThinkingInProgress(false);
}, 1000);
}
return () => clearInterval(intervalId);
}, [thinkingInProgress]);
const onExpandCollapseClick = (event) => {
const parent = event.currentTarget.parentElement;
if (parent.classList.contains('expanded')) {
parent.classList.remove('expanded');
}
else {
parent.classList.add('expanded');
}
};
return (React.createElement("div", { className: `chat-message chat-message-${msg.from}`, "data-render-count": renderCount },
React.createElement("div", { className: "chat-message-header" },
React.createElement("div", { className: "chat-message-from" },
((_a = msg.participant) === null || _a === void 0 ? void 0 : _a.iconPath) && (React.createElement("div", { className: `chat-message-from-icon ${((_b = msg.participant) === null || _b === void 0 ? void 0 : _b.id) === 'default' ? 'chat-message-from-icon-default' : ''} ${isDarkTheme() ? 'dark' : ''}` },
React.createElement("img", { src: msg.participant.iconPath }))),
React.createElement("div", { className: "chat-message-from-title" }, msg.from === 'user'
? 'User'
: ((_c = msg.participant) === null || _c === void 0 ? void 0 : _c.name) || 'AI Assistant'),
React.createElement("div", { className: "chat-message-from-progress", style: { display: `${props.showGenerating ? 'visible' : 'none'}` } },
React.createElement("div", { className: "loading-ellipsis" }, "Generating"))),
React.createElement("div", { className: "chat-message-timestamp" }, timestamp)),
React.createElement("div", { className: "chat-message-content" },
groupedContents.map((item, index) => {
switch (item.type) {
case ResponseStreamDataType.Markdown:
case ResponseStreamDataType.MarkdownPart:
return (React.createElement(React.Fragment, null,
item.reasoningContent && (React.createElement("div", { className: "expandable-content" },
React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
React.createElement(VscTriangleDown, { className: "expanded-icon" }),
' ',
item.reasoningFinished
? 'Thought'
: `Thinking (${Math.floor(item.reasoningTime)} s)`),
React.createElement("div", { className: "expandable-content-text" },
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.reasoningContent)))),
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.content),
item.contentDetail ? (React.createElement("div", { className: "expandable-content" },
React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
React.createElement(VscTriangleDown, { className: "expanded-icon" }),
' ',
item.contentDetail.title),
React.createElement("div", { className: "expandable-content-text" },
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.contentDetail.content)))) : null));
case ResponseStreamDataType.Image:
return (React.createElement("div", { className: "chat-response-img", key: `key-${index}` },
React.createElement("img", { src: item.content })));
case ResponseStreamDataType.HTMLFrame:
return (React.createElement(ChatResponseHTMLFrame, { index: index, source: item.content.source, height: item.content.height }));
case ResponseStreamDataType.Button:
return (React.createElement("div", { className: "chat-response-button", key: `key-${index}` },
React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => runCommand(item.content.commandId, item.content.args) },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.title))));
case ResponseStreamDataType.Anchor:
return (React.createElement("div", { className: "chat-response-anchor", key: `key-${index}` },
React.createElement("a", { href: item.content.uri, target: "_blank" }, item.content.title)));
case ResponseStreamDataType.Progress:
// show only if no more message available
return index === groupedContents.length - 1 ? (React.createElement("div", { className: "chat-response-progress", key: `key-${index}` },
"\u2713 ",
item.content)) : null;
case ResponseStreamDataType.Confirmation:
return answeredForms.get(item.id) ===
'confirmed' ? null : answeredForms.get(item.id) ===
'canceled' ? (React.createElement("div", null, "\u2716 Canceled")) : (React.createElement("div", { className: "chat-confirmation-form", key: `key-${index}` },
item.content.title ? (React.createElement("div", null,
React.createElement("b", null, item.content.title))) : null,
item.content.message ? (React.createElement("div", null, item.content.message)) : null,
React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => {
markFormConfirmed(item.id);
runCommand('notebook-intelligence:chat-user-input', item.content.confirmArgs);
} },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.confirmLabel)),
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
markFormCanceled(item.id);
runCommand('notebook-intelligence:chat-user-input', item.content.cancelArgs);
} },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.cancelLabel))));
}
return null;
}),
msg.notebookLink && (React.createElement("a", { className: "copilot-generated-notebook-link", "data-ref": msg.notebookLink, onClick: openNotebook }, "open notebook")))));
}
const MemoizedChatResponse = memo(ChatResponse);
async function submitCompletionRequest(request, responseEmitter) {
switch (request.type) {
case RunChatCompletionType.Chat:
return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.filename || 'Untitled.ipynb', request.additionalContext || [], request.chatMode, request.toolSelections || {}, responseEmitter);
case RunChatCompletionType.ExplainThis:
case RunChatCompletionType.FixThis:
case RunChatCompletionType.ExplainThisOutput:
case RunChatCompletionType.TroubleshootThisOutput: {
return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.filename || 'Untitled.ipynb', [], 'ask', {}, responseEmitter);
}
case RunChatCompletionType.GenerateCode:
return NBIAPI.generateCode(request.chatId, request.content, request.prefix || '', request.suffix || '', request.existingCode || '', request.language || 'python', request.filename || 'Untitled.ipynb', responseEmitter);
}
}
function CheckBoxItem(props) {
const indent = props.indent || 0;
return (React.createElement("div", { className: `checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`, title: props.title, onClick: event => props.onClick(event) },
React.createElement("div", { className: "checkbox-item-toggle" },
props.checked ? (React.createElement(MdCheckBox, { className: "checkbox-icon" })) : (React.createElement(MdOutlineCheckBoxOutlineBlank, { className: "checkbox-icon" })),
props.label),
props.title && (React.createElement("div", { className: "checkbox-item-description" }, props.title))));
}
function SidebarComponent(props) {
const [chatMessages, setChatMessages] = useState([]);
const [prompt, setPrompt] = useState('');
const [draftPrompt, setDraftPrompt] = useState('');
const messagesEndRef = useRef(null);
const [ghLoginStatus, setGHLoginStatus] = useState(GitHubCopilotLoginStatus.NotLoggedIn);
const [loginClickCount, _setLoginClickCount] = useState(0);
const [copilotRequestInProgress, setCopilotRequestInProgress] = useState(false);
const [showPopover, setShowPopover] = useState(false);
const [originalPrefixes, setOriginalPrefixes] = useState([]);
const [prefixSuggestions, setPrefixSuggestions] = useState([]);
const [selectedPrefixSuggestionIndex, setSelectedPrefixSuggestionIndex] = useState(0);
const promptInputRef = useRef(null);
const [promptHistory, setPromptHistory] = useState([]);
// position on prompt history stack
const [promptHistoryIndex, setPromptHistoryIndex] = useState(0);
const [chatId, setChatId] = useState(UUID.uuid4());
const lastMessageId = useRef('');
const lastRequestTime = useRef(new Date());
const [contextOn, setContextOn] = useState(false);
const [activeDocumentInfo, setActiveDocumentInfo] = useState(null);
const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
const telemetryEmitter = props.getTelemetryEmitter();
const [chatMode, setChatMode] = useState('ask');
const [toolSelectionTitle, setToolSelectionTitle] = useState('Tool selection');
const [selectedToolCount, setSelectedToolCount] = useState(0);
const [notebookExecuteToolSelected, setNotebookExecuteToolSelected] = useState(false);
const [toolConfig, setToolConfig] = useState({
builtinToolsets: [
{ id: BuiltinToolsetType.NotebookEdit, name: 'Notebook edit' },
{ id: BuiltinToolsetType.NotebookExecute, name: 'Notebook execute' }
],
mcpServers: [],
extensions: []
});
const [showModeTools, setShowModeTools] = useState(false);
const toolSelectionsInitial = {
builtinToolsets: [BuiltinToolsetType.NotebookEdit],
mcpServers: {},
extensions: {}
};
const toolSelectionsEmpty = {
builtinToolsets: [],
mcpServers: {},
extensions: {}
};
const [toolSelections, setToolSelections] = useState(toolSelectionsInitial);
const [hasExtensionTools, setHasExtensionTools] = useState(false);
const [lastScrollTime, setLastScrollTime] = useState(0);
const [scrollPending, setScrollPending] = useState(false);
NBIAPI.configChanged.connect(() => {
setToolConfig(NBIAPI.config.toolConfig);
});
useEffect(() => {
let hasTools = false;
for (const extension of toolConfig.extensions) {
if (extension.toolsets.length > 0) {
hasTools = true;
break;
}
}
setHasExtensionTools(hasTools);
}, [toolConfig]);
useEffect(() => {
const builtinToolSelCount = toolSelections.builtinToolsets.length;
let mcpServerToolSelCount = 0;
let extensionToolSelCount = 0;
for (const serverId in toolSelections.mcpServers) {
const mcpServerTools = toolSelections.mcpServers[serverId];
mcpServerToolSelCount += mcpServerTools.length;
}
for (const extensionId in toolSelections.extensions) {
const extensionToolsets = toolSelections.extensions[extensionId];
for (const toolsetId in extensionToolsets) {
const toolsetTools = extensionToolsets[toolsetId];
extensionToolSelCount += toolsetTools.length;
}
}
const typeCounts = [];
if (builtinToolSelCount > 0) {
typeCounts.push(`${builtinToolSelCount} built-in`);
}
if (mcpServerToolSelCount > 0) {
typeCounts.push(`${mcpServerToolSelCount} mcp`);
}
if (extensionToolSelCount > 0) {
typeCounts.push(`${extensionToolSelCount} ext`);
}
setSelectedToolCount(builtinToolSelCount + mcpServerToolSelCount + extensionToolSelCount);
setNotebookExecuteToolSelected(toolSelections.builtinToolsets.includes(BuiltinToolsetType.NotebookExecute));
setToolSelectionTitle(typeCounts.length === 0
? 'Tool selection'
: `Tool selection (${typeCounts.join(', ')})`);
}, [toolSelections]);
const onClearToolsButtonClicked = () => {
setToolSelections(toolSelectionsEmpty);
};
const getBuiltinToolsetState = (toolsetName) => {
return toolSelections.builtinToolsets.includes(toolsetName);
};
const setBuiltinToolsetState = (toolsetName, enabled) => {
const newConfig = { ...toolSelections };
if (enabled) {
if (!toolSelections.builtinToolsets.includes(toolsetName)) {
newConfig.builtinToolsets.push(toolsetName);
}
}
else {
const index = newConfig.builtinToolsets.indexOf(toolsetName);
if (index !== -1) {
newConfig.builtinToolsets.splice(index, 1);
}
}
setToolSelections(newConfig);
};
const anyMCPServerToolSelected = (id) => {
if (!(id in toolSelections.mcpServers)) {
return false;
}
return toolSelections.mcpServers[id].length > 0;
};
const getMCPServerState = (id) => {
if (!(id in toolSelections.mcpServers)) {
return false;
}
const mcpServer = toolConfig.mcpServers.find(server => server.id === id);
const selectedServerTools = toolSelections.mcpServers[id];
for (const tool of mcpServer.tools) {
if (!selectedServerTools.includes(tool.name)) {
return false;
}
}
return true;
};
const onMCPServerClicked = (id) => {
if (anyMCPServerToolSelected(id)) {
const newConfig = { ...toolSelections };
delete newConfig.mcpServers[id];
setToolSelections(newConfig);
}
else {
const mcpServer = toolConfig.mcpServers.find(server => server.id === id);
const newConfig = { ...toolSelections };
newConfig.mcpServers[id] = structuredClone(mcpServer.tools.map((tool) => tool.name));
setToolSelections(newConfig);
}
};
const getMCPServerToolState = (serverId, toolId) => {
if (!(serverId in toolSelections.mcpServers)) {
return false;
}
const selectedServerTools = toolSelections.mcpServers[serverId];
return selectedServerTools.includes(toolId);
};
const setMCPServerToolState = (serverId, toolId, checked) => {
const newConfig = { ...toolSelections };
if (checked && !(serverId in newConfig.mcpServers)) {
newConfig.mcpServers[serverId] = [];
}
const selectedServerTools = newConfig.mcpServers[serverId];
if (checked) {
selectedServerTools.push(toolId);
}
else {
const index = selectedServerTools.indexOf(toolId);
if (index !== -1) {
selectedServerTools.splice(index, 1);
}
}
setToolSelections(newConfig);
};
// all toolsets and tools of the extension are selected
const getExtensionState = (extensionId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
const extension = toolConfig.extensions.find(extension => extension.id === extensionId);
for (const toolset of extension.toolsets) {
if (!getExtensionToolsetState(extensionId, toolset.id)) {
return false;
}
}
return true;
};
const getExtensionToolsetState = (extensionId, toolsetId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
if (!(toolsetId in toolSelections.extensions[extensionId])) {
return false;
}
const extension = toolConfig.extensions.find(ext => ext.id === extensionId);
const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
const selectedToolsetTools = toolSelections.extensions[extensionId][toolsetId];
for (const tool of extensionToolset.tools) {
if (!selectedToolsetTools.includes(tool.name)) {
return false;
}
}
return true;
};
const anyExtensionToolsetSelected = (extensionId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
return Object.keys(toolSelections.extensions[extensionId]).length > 0;
};
const onExtensionClicked = (extensionId) => {
if (anyExtensionToolsetSelected(extensionId)) {
const newConfig = { ...toolSelections };
delete newConfig.extensions[extensionId];
setToolSelections(newConfig);
}
else {
const newConfig = { ...toolSelections };
const extension = toolConfig.extensions.find(ext => ext.id === extensionId);
if (extensionId in newConfig.extensions) {
delete newConfig.extensions[extensionId];
}
newConfig.extensions[extensionId] = {};
for (const toolset of extension.toolsets) {
newConfig.extensions[extensionId][toolset.id] = structuredClone(toolset.tools.map((tool) => tool.name));
}
setToolSelections(newConfig);
}
};
const anyExtensionToolsetToolSelected = (extensionId, toolsetId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
if (!(toolsetId in toolSelections.extensions[extensionId])) {
return false;
}
return toolSelections.extensions[extensionId][toolsetId].length > 0;
};
const onExtensionToolsetClicked = (extensionId, toolsetId) => {
if (anyExtensionToolsetToolSelected(extensionId, toolsetId)) {
const newConfig = { ...toolSelections };
if (toolsetId in newConfig.extensions[extensionId]) {
delete newConfig.extensions[extensionId][toolsetId];
}
setToolSelections(newConfig);
}
else {
const extension = toolConfig.extensions.find(ext => ext.id === extensionId);
const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
const newConfig = { ...toolSelections };
if (!(extensionId in newConfig.extensions)) {
newConfig.extensions[extensionId] = {};
}
newConfig.extensions[extensionId][toolsetId] = structuredClone(extensionToolset.tools.map((tool) => tool.name));
setToolSelections(newConfig);
}
};
const getExtensionToolsetToolState = (extensionId, toolsetId, toolId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
const selectedExtensionToolsets = toolSelections.extensions[extensionId];
if (!(toolsetId in selectedExtensionToolsets)) {
return false;
}
const selectedServerTools = selectedExtensionToolsets[toolsetId];
return selectedServerTools.includes(toolId);
};
const setExtensionToolsetToolState = (extensionId, toolsetId, toolId, checked) => {
const newConfig = { ...toolSelections };
if (checked && !(extensionId in newConfig.extensions)) {
newConfig.extensions[extensionId] = {};
}
if (checked && !(toolsetId in newConfig.extensions[extensionId])) {
newConfig.extensions[extensionId][toolsetId] = [];
}
const selectedTools = newConfig.extensions[extensionId][toolsetId];
if (checked) {
selectedTools.push(toolId);
}
else {
const index = selectedTools.indexOf(toolId);
if (index !== -1) {
selectedTools.splice(index, 1);
}
}
setToolSelections(newConfig);
};
useEffect(() => {
const prefixes = [];
if (chatMode !== 'ask') {
prefixes.push('/clear');
setOriginalPrefixes(prefixes);
setPrefixSuggestions(prefixes);
return;
}
const chatParticipants = NBIAPI.config.chatParticipants;
for (const participant of chatParticipants) {
const id = participant.id;
const commands = participant.commands;
const participantPrefix = id === 'default' ? '' : `@${id}`;
if (participantPrefix !== '') {
prefixes.push(participantPrefix);
}
const commandPrefix = participantPrefix === '' ? '' : `${participantPrefix} `;
for (const command of commands) {
prefixes.push(`${commandPrefix}/${command}`);
}
}
setOriginalPrefixes(prefixes);
setPrefixSuggestions(prefixes);
}, [chatMode]);
useEffect(() => {
const fetchData = () => {
setGHLoginStatus(NBIAPI.getLoginStatus());
};
fetchData();
const intervalId = setInterval(fetchData, 1000);
return () => clearInterval(intervalId);
}, [loginClickCount]);
useEffect(() => {
setSelectedPrefixSuggestionIndex(0);
}, [prefixSuggestions]);
const onPromptChange = (event) => {
const newPrompt = event.target.value;
setPrompt(newPrompt);
const trimmedPrompt = newPrompt.trimStart();
if (trimmedPrompt === '@' || trimmedPrompt === '/') {
setShowPopover(true);
filterPrefixSuggestions(trimmedPrompt);
}
else if (trimmedPrompt.startsWith('@') ||
trimmedPrompt.startsWith('/') ||
trimmedPrompt === '') {
filterPrefixSuggestions(trimmedPrompt);
}
else {
setShowPopover(false);
}
};
const applyPrefixSuggestion = (prefix) => {
var _a;
if (prefix.includes(prompt)) {
setPrompt(`${prefix} `);
}
else {
setPrompt(`${prefix} ${prompt} `);
}
setShowPopover(false);
(_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
setSelectedPrefixSuggestionIndex(0);
};
const prefixSuggestionSelected = (event) => {
const prefix = event.target.dataset['value'];
applyPrefixSuggestion(prefix);
};
const handleSubmitStopChatButtonClick = async () => {
setShowModeTools(false);
if (!copilotRequestInProgress) {
handleUserInputSubmit();
}
else {
handleUserInputCancel();
}
};
const handleSettingsButtonClick = async () => {
setShowModeTools(false);
props
.getApp()
.commands.execute('notebook-intelligence:open-configuration-dialog');
};
const handleChatToolsButtonClick = async () => {
if (!showModeTools) {
NBIAPI.fetchCapabilities();
}
setShowModeTools(!showModeTools);
};
const handleUserInputSubmit = async () => {
setPromptHistoryIndex(promptHistory.length + 1);
setPromptHistory([...promptHistory, prompt]);
setShowPopover(false);
const promptPrefixParts = [];
const promptParts = prompt.split(' ');
if (promptParts.length > 1) {
for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
const part = promptParts[i];
if (part.startsWith('@') || part.startsWith('/')) {
promptPrefixParts.push(part);
}
}
}
const promptPrefix = promptPrefixParts.length > 0 ? promptPrefixParts.join(' ') + ' ' : '';
lastMessageId.current = UUID.uuid4();
lastRequestTime.current = new Date();
const newList = [
...chatMessages,
{
id: lastMessageId.current,
date: new Date(),
from: 'user',
contents: [
{
id: UUID.uuid4(),
type: ResponseStreamDataType.Markdown,
content: prompt,
created: new Date()
}
]
}
];
setChatMessages(newList);
if (prompt.startsWith('/clear')) {
setChatMessages([]);
setPrompt('');
resetChatId();
resetPrefixSuggestions();
setPromptHistory([]);
setPromptHistoryIndex(0);
NBIAPI.sendWebSocketMessage(UUID.uuid4(), RequestDataType.ClearChatHistory, { chatId });
return;
}
setCopilotRequestInProgress(true);
const activeDocInfo = props.getActiveDocumentInfo();
const extractedPrompt = prompt;
const contents = [];
const app = props.getApp();
const additionalContext = [];
if (contextOn && (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename)) {
const selection = activeDocumentInfo.selection;
const textSelected = selection &&
!(selection.start.line === selection.end.line &&
selection.start.column === selection.end.column);
additionalContext.push({
type: ContextType.CurrentFile,
content: props.getActiveSelectionContent(),
currentCellContents: textSelected
? null
: props.getCurrentCellContents(),
filePath: activeDocumentInfo.filePath,
cellIndex: activeDocumentInfo.activeCellIndex,
startLine: selection ? selection.start.line + 1 : 1,
endLine: selection ? selection.end.line + 1 : 1
});
}
submitCompletionRequest({
messageId: lastMessageId.current,
chatId,
type: RunChatCompletionType.Chat,
content: extractedPrompt,
language: activeDocInfo.language,
filename: activeDocInfo.filename,
additionalContext,
chatMode,
toolSelections: toolSelections
}, {
emit: async (response) => {
var _a, _b, _c, _d, _e;
if (response.id !== lastMessageId.current) {
return;
}
let responseMessage = '';
if (response.type === BackendMessageType.StreamMessage) {
const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
if (!delta) {
return;
}
if (delta['nbiContent']) {
const nbiContent = delta['nbiContent'];
contents.push({
id: UUID.uuid4(),
type: nbiContent.type,
content: nbiContent.content,
contentDetail: nbiContent.detail,
created: new Date(response.created)
});
}
else {
responseMessage =
(_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
if (!responseMessage) {
return;
}
contents.push({
id: UUID.uuid4(),
type: ResponseStreamDataType.MarkdownPart,
content: responseMessage,
created: new Date(response.created)
});
}
}
else if (response.type === BackendMessageType.StreamEnd) {
setCopilotRequestInProgress(false);
const timeElapsed = (new Date().getTime() - lastRequestTime.current.getTime()) / 1000;
telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.ChatResponse,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
timeElapsed
}
});
}
else if (response.type === BackendMessageType.RunUICommand) {
const messageId = response.id;
const result = await app.commands.execute(response.data.commandId, response.data.args);
const data = {
callback_id: response.data.callback_id,
result: result || 'void'
};
try {
JSON.stringify(data);
}
catch (error) {
data.result = 'Could not serialize the result';
}
NBIAPI.sendWebSocketMessage(messageId, RequestDataType.RunUICommandResponse, data);
}
setChatMessages([
...newList,
{
id: UUID.uuid4(),
date: new Date(),
from: 'copilot',
contents: contents,
participant: NBIAPI.config.chatParticipants.find(participant => {
return participant.id === response.participant;
})
}
]);
}
});
const newPrompt = prompt.startsWith('/settings') ? '' : promptPrefix;
setPrompt(newPrompt);
filterPrefixSuggestions(newPrompt);
telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.ChatRequest,
data: {
chatMode,
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
prompt: extractedPrompt
}
});
};
const handleUserInputCancel = async () => {
NBIAPI.sendWebSocketMessage(lastMessageId.current, RequestDataType.CancelChatRequest, { chatId });
lastMessageId.current = '';
setCopilotRequestInProgress(false);
};
const filterPrefixSuggestions = (prmpt) => {
const userInput = prmpt.trimStart();
if (userInput === '') {
setPrefixSuggestions(originalPrefixes);
}
else {
setPrefixSuggestions(originalPrefixes.filter(prefix => prefix.includes(userInput)));
}
};
const resetPrefixSuggestions = () => {
setPrefixSuggestions(originalPrefixes);
setSelectedPrefixSuggestionIndex(0);
};
const resetChatId = () => {
setChatId(UUID.uuid4());
};
const onPromptKeyDown = async (event) => {
if (event.key === 'Enter') {
event.stopPropagation();
event.preventDefault();
if (showPopover) {
applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
return;
}
setSelectedPrefixSuggestionIndex(0);
handleSubmitStopChatButtonClick();
}
else if (event.key === 'Tab') {
if (showPopover) {
event.stopPropagation();
event.preventDefault();
applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
return;
}
}
else if (event.key === 'Escape') {
event.stopPropagation();
event.preventDefault();
setShowPopover(false);
setShowModeTools(false);
setSelectedPrefixSuggestionIndex(0);
}
else if (event.key === 'ArrowUp') {
event.stopPropagation();
event.preventDefault();
if (showPopover) {
setSelectedPrefixSuggestionIndex((selectedPrefixSuggestionIndex - 1 + prefixSuggestions.length) %
prefixSuggestions.length);
return;
}
setShowPopover(false);
// first time up key press
if (promptHistory.length > 0 &&
promptHistoryIndex === promptHistory.length) {
setDraftPrompt(prompt);
}
if (promptHistory.length > 0 &&
promptHistoryIndex > 0 &&
promptHistoryIndex <= promptHistory.length) {
const prevPrompt = promptHistory[promptHistoryIndex - 1];
const newIndex = promptHistoryIndex - 1;
setPrompt(prevPrompt);
setPromptHistoryIndex(newIndex);
}
}
else if (event.key === 'ArrowDown') {
event.stopPropagation();
event.preventDefault();
if (showPopover) {
setSelectedPrefixSuggestionIndex((selectedPrefixSuggestionIndex + 1 + prefixSuggestions.length) %
prefixSuggestions.length);
return;
}
setShowPopover(false);
if (promptHistory.length > 0 &&
promptHistoryIndex >= 0 &&
promptHistoryIndex < promptHistory.length) {
if (promptHistoryIndex === promptHistory.length - 1) {
setPrompt(draftPrompt);
setPromptHistoryIndex(promptHistory.length);
return;
}
const prevPrompt = promptHistory[promptHistoryIndex + 1];
const newIndex = promptHistoryIndex + 1;
setPrompt(prevPrompt);
setPromptHistoryIndex(newIndex);
}
}
};
// Throttle scrollMessagesToBottom to only scroll every 500ms
const SCROLL_THROTTLE_TIME = 1000;
const scrollMessagesToBottom = () => {
var _a;
const now = Date.now();
if (now - lastScrollTime >= SCROLL_THROTTLE_TIME) {
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
setLastScrollTime(now);
}
else if (!scrollPending) {
setScrollPending(true);
setTimeout(() => {
var _a;
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
setLastScrollTime(Date.now());
setScrollPending(false);
}, SCROLL_THROTTLE_TIME - (now - lastScrollTime));
}
};
const handleConfigurationClick = async () => {
props
.getApp()
.commands.execute('notebook-intelligence:open-configuration-dialog');
};
const handleLoginClick = async () => {
props
.getApp()
.commands.execute('notebook-intelligence:open-github-copilot-login-dialog');
};
useEffect(() => {
scrollMessagesToBottom();
}, [chatMessages]);
const promptRequestHandler = useCallback((eventData) => {
const request = eventData.detail;
request.chatId = chatId;
let message = '';
switch (request.type) {
case RunChatCompletionType.ExplainThis:
message = `Explain this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
break;
case RunChatCompletionType.FixThis:
message = `Fix this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
break;
case RunChatCompletionType.ExplainThisOutput:
message = `Explain this notebook cell output: \n\`\`\`\n${request.content}\n\`\`\`\n`;
break;
case RunChatCompletionType.TroubleshootThisOutput:
message = `Troubleshoot errors reported in the notebook cell output: \n\`\`\`\n${request.content}\n\`\`\`\n`;
break;
}
const messageId = UUID.uuid4();
request.messageId = messageId;
const newList = [
...chatMessages,
{
id: messageId,
date: new Date(),
from: 'user',
contents: [
{
id: messageId,
type: ResponseStreamDataType.Markdown,
content: message,
created: new Date()
}
]
}
];
setChatMessages(newList);
setCopilotRequestInProgress(true);
const contents = [];
submitCompletionRequest(request, {
emit: response => {
var _a, _b, _c, _d, _e;
if (response.type === BackendMessageType.StreamMessage) {
const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
if (!delta) {
return;
}
const responseMessage = (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']