UNPKG

@notebook-intelligence/notebook-intelligence

Version:
1,056 lines 94.7 kB
// 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']