From a590a150a9902d0c867c8822e4b88b1be3db105c Mon Sep 17 00:00:00 2001 From: amitjoshi438 <54068463+amitjoshi438@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:40:40 +0530 Subject: [PATCH] [Copilot] Show user selected code along with prompt in copilot panel (#730) * getting info about selected code * showing label for selected lines of code * add flag to disable feature * updated responsiveness * code formatting fix * removed log statement * handles empty selections * passing and showing user selected code to copilot * setting fixed vertical height for user code * removed redundant code * added comment * added const for explainCode msg * sending localized string to copilot webview --------- Co-authored-by: amitjoshi Co-authored-by: tyaginidhi --- src/common/copilot/IntelligenceApiService.ts | 2 +- src/common/copilot/PowerPagesCopilot.ts | 35 +++++-- src/common/copilot/assets/copilotStrings.ts | 11 +++ src/common/copilot/assets/scripts/copilot.js | 99 ++++++++------------ src/common/copilot/assets/styles/copilot.css | 9 ++ src/common/copilot/constants.ts | 1 + 6 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 src/common/copilot/assets/copilotStrings.ts diff --git a/src/common/copilot/IntelligenceApiService.ts b/src/common/copilot/IntelligenceApiService.ts index 9c09ee2f..22e69145 100644 --- a/src/common/copilot/IntelligenceApiService.ts +++ b/src/common/copilot/IntelligenceApiService.ts @@ -33,7 +33,7 @@ export async function sendApiRequest(userPrompt: string, activeFileParams: IActi "dataverseEntity": activeFileParams.dataverseEntity, "entityField": activeFileParams.entityField, "fieldType": activeFileParams.fieldType, - "activeFileContent": '', + "activeFileContent": '', //TODO: Add active file content (selected code) "targetEntity": entityName, "targetColumns": entityColumns, "clientType": clientType, diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index 1a3a8c6a..05c3c569 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -10,7 +10,7 @@ import { dataverseAuthentication, intelligenceAPIAuthentication } from "../../we import { v4 as uuidv4 } from 'uuid' import { PacWrapper } from "../../client/pac/PacWrapper"; import { ITelemetry } from "../../client/telemetry/ITelemetry"; -import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_UNAVAILABLE, CopilotDisclaimer, CopilotStylePathSegments, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap, PAC_SUCCESS, SELECTED_CODE_INFO_ENABLED, WebViewMessage, sendIconSvg } from "./constants"; +import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_UNAVAILABLE, CopilotDisclaimer, CopilotStylePathSegments, DataverseEntityNameMap, EXPLAIN_CODE, EntityFieldMap, FieldTypeMap, PAC_SUCCESS, SELECTED_CODE_INFO_ENABLED, WebViewMessage, sendIconSvg } from "./constants"; import { IActiveFileParams, IActiveFileData, IOrgInfo } from './model'; import { escapeDollarSign, getLastThreePartsOfFileName, getNonce, getSelectedCode, getSelectedCodeLineRange, getUserName, openWalkthrough, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; import { CESUserFeedback } from "./user-feedback/CESSurvey"; @@ -22,6 +22,7 @@ import { INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID } from "../../web/client/common import { getIntelligenceEndpoint } from "../ArtemisService"; import TelemetryReporter from "@vscode/extension-telemetry"; import { getEntityColumns, getEntityName } from "./dataverseMetadata"; +import { COPILOT_STRINGS } from "./assets/copilotStrings"; let intelligenceApiToken: string; let userID: string; // Populated from PAC or intelligence API @@ -68,16 +69,28 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { if (SELECTED_CODE_INFO_ENABLED) { //TODO: Remove this check once the feature is ready + + const handleSelectionChange = async (commandType: string) => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + const selectedCode = getSelectedCode(editor); + const selectedCodeLineRange = getSelectedCodeLineRange(editor); + if(commandType === EXPLAIN_CODE && selectedCode.length === 0) { + // Show a message if the selection is empty and don't send the message to webview + vscode.window.showInformationMessage(vscode.l10n.t('Selection is empty!')); + return; + } + this.sendMessageToWebview({ type: commandType, value: { start: selectedCodeLineRange.start, end: selectedCodeLineRange.end, selectedCode: selectedCode } }); + }; + + this._disposables.push( + vscode.window.onDidChangeTextEditorSelection(() => handleSelectionChange("selectedCodeInfo")) + ); + this._disposables.push( - vscode.window.onDidChangeTextEditorSelection(async () => { - const editor = vscode.window.activeTextEditor; - if (!editor) { - return; - } - const selectedCode = getSelectedCode(editor); - const selectedCodeLineRange = getSelectedCodeLineRange(editor); - this.sendMessageToWebview({ type: "selectedCodeInfo", value: {start: selectedCodeLineRange.start, end: selectedCodeLineRange.end, selectedCode: selectedCode} }); - }) + vscode.commands.registerCommand("powerpages.copilot.explain", () => handleSelectionChange(EXPLAIN_CODE)) ); } @@ -165,6 +178,8 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { webviewView.webview.onDidReceiveMessage(async (data) => { switch (data.type) { case "webViewLoaded": { + // Send the localized strings to the copilot webview + this.sendMessageToWebview({type: 'copilotStrings', value: COPILOT_STRINGS}) if (this.aibEndpoint === COPILOT_UNAVAILABLE) { this.sendMessageToWebview({ type: 'Unavailable' }); return; diff --git a/src/common/copilot/assets/copilotStrings.ts b/src/common/copilot/assets/copilotStrings.ts new file mode 100644 index 00000000..4a6e6037 --- /dev/null +++ b/src/common/copilot/assets/copilotStrings.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + + +import vscode from "vscode"; + +export const COPILOT_STRINGS = { + EXPLAIN_CODE_PROMPT: vscode.l10n.t('Explain the following code:'), +} diff --git a/src/common/copilot/assets/scripts/copilot.js b/src/common/copilot/assets/scripts/copilot.js index ffec7c35..c6d38907 100644 --- a/src/common/copilot/assets/scripts/copilot.js +++ b/src/common/copilot/assets/scripts/copilot.js @@ -20,7 +20,8 @@ let isCopilotEnabled = true; let isLoggedIn = false; let apiResponseInProgress = false; - + let selectedCode = ""; + let copilotStrings = {}; const inputHistory = []; @@ -62,7 +63,7 @@ vscode.postMessage({ type: "webViewLoaded" }); - function parseCodeBlocks(responseText) { + function parseCodeBlocks(responseText, isUserCode) { const resultDiv = document.createElement("div"); let codeLineCount = 0; @@ -84,6 +85,9 @@ const codeDiv = document.createElement("div"); codeDiv.classList.add("code-division"); + + isUserCode ? codeDiv.classList.add("user-code") : codeDiv.classList.add("copilot-code"); + let codeBlock = responseText[i].code; codeLineCount += countLines(codeBlock); @@ -111,35 +115,6 @@ return lines.length; } - function formatCodeBlocks(responseText) { - const blocks = responseText.split("```"); - const resultDiv = document.createElement("div"); - - for (let i = 0; i < blocks.length; i++) { - if (i % 2 === 0) { - // Handle text blocks - const textDiv = document.createElement("div"); - textDiv.innerText = blocks[i]; - resultDiv.appendChild(textDiv); - } else { - // Handle code blocks - const codeDiv = document.createElement("div"); - codeDiv.classList.add("code-division"); - codeDiv.appendChild(createActionWrapper(blocks[i])); - - const preFormatted = document.createElement("pre"); - const codeSnip = document.createElement("code"); - codeSnip.innerText = blocks[i]; - preFormatted.appendChild(codeSnip); - - codeDiv.appendChild(preFormatted); - resultDiv.appendChild(codeDiv); - } - } - resultDiv.classList.add("result-div"); - return resultDiv; - } - function createActionWrapper(code) { const actionWrapper = document.createElement("div"); actionWrapper.classList.add("action-wrapper"); @@ -195,7 +170,7 @@ makerElement.appendChild(user); messageElement.appendChild(makerElement); makerElement.appendChild(document.createElement("br")); - messageElement.appendChild(formatCodeBlocks(message)); + messageElement.appendChild(parseCodeBlocks(message, true)); messageElement.classList.add("message", "user-message"); @@ -430,6 +405,10 @@ const message = event.data; // The JSON data our extension sent switch (message.type) { + case "copilotStrings": { + copilotStrings = message.value; //Localized string values object + break; + } case "apiResponse": { apiResponseHandler.updateResponse(message.value); apiResponseInProgress = false; @@ -487,13 +466,21 @@ } case "selectedCodeInfo": { const chatInputLabel = document.getElementById("input-label-id"); - if (message.value.start == message.value.end && message.value.selectedCode.length == 0) { + selectedCode = message.value.selectedCode; + if (message.value.start == message.value.end && selectedCode.length == 0) { chatInputLabel.classList.add("hide"); break; } chatInputLabel.classList.remove("hide"); chatInputLabel.innerText = `Lines: ${message.value.start + 1} - ${message.value.end + 1} selected`; + break; } + case "explainCode": { + selectedCode = message.value.selectedCode; + const explainPrompt = copilotStrings.EXPLAIN_CODE_PROMPT; + processUserInput(explainPrompt); + } + } }); @@ -523,35 +510,36 @@ vscode.postMessage({ type: "walkthrough" }); } - - SendButton?.addEventListener("click", () => { - if(apiResponseInProgress) { + function processUserInput(input) { + if (apiResponseInProgress) { return; } - if ((chatInput).value.trim()) { - handleUserMessage((chatInput).value); + if (input) { + const userPrompt = [{ displayText: input, code: selectedCode }]; + handleUserMessage(userPrompt); chatInput.disabled = true; - saveInputToHistory(chatInput.value); + saveInputToHistory(input); apiResponseInProgress = true; - getApiResponse((chatInput).value); - (chatInput).value = ""; - (chatInput).focus(); + getApiResponse(input + ': ' + selectedCode); //TODO: userPrompt object should be passed + chatInput.value = ""; + chatInput.focus(); } + } + + + SendButton?.addEventListener("click", () => { + processUserInput(chatInput.value.trim()); }); chatInput.addEventListener("keydown", (event) => { - if(apiResponseInProgress) { + if(apiResponseInProgress && event.key !== "Enter") { return; } - if (event.key === "Enter" && (chatInput).value.trim()) { - handleUserMessage((chatInput).value); - chatInput.disabled = true; - saveInputToHistory(chatInput.value); - apiResponseInProgress = true; - getApiResponse((chatInput).value); - (chatInput).value = ""; + if (event.key === "Enter") { + processUserInput(chatInput.value.trim()); } }); + chatMessages.addEventListener("click", handleFeedbackClick); function handleFeedbackClick(event) { @@ -596,15 +584,8 @@ } function handleSuggestionsClick() { - if(apiResponseInProgress) { - return; - } - const userPrompt = this.textContent.trim(); - handleUserMessage(userPrompt); - chatInput.disabled = true; - saveInputToHistory(userPrompt); - apiResponseInProgress = true; - getApiResponse(userPrompt); + const suggestedPrompt = this.innerText.trim(); + processUserInput(suggestedPrompt); } chatInput.addEventListener('keydown', handleArrowKeys); diff --git a/src/common/copilot/assets/styles/copilot.css b/src/common/copilot/assets/styles/copilot.css index 449bc9c7..10727bf8 100644 --- a/src/common/copilot/assets/styles/copilot.css +++ b/src/common/copilot/assets/styles/copilot.css @@ -165,6 +165,15 @@ body { padding: 20px 5px 1px 10px; margin: 10px 0px; border-radius: 4px; +} + +.user-code { + background-color: var(--vscode-editor-background); + max-height: 200px; + overflow: scroll; +} + +.copilot-code { background-color: var(--vscode-sideBar-background) } diff --git a/src/common/copilot/constants.ts b/src/common/copilot/constants.ts index 94ac5e96..c52224e7 100644 --- a/src/common/copilot/constants.ts +++ b/src/common/copilot/constants.ts @@ -26,6 +26,7 @@ export const INPUT_CONTENT_FILTERED = 'InputContentFiltered'; export const PROMPT_LIMIT_EXCEEDED = 'PromptLimitExceeded'; export const INVALID_INFERENCE_INPUT = 'InvalidInferenceInput'; export const COPILOT_NOTIFICATION_DISABLED = 'isCopilotNotificationDisabled' +export const EXPLAIN_CODE = 'explainCode'; export const SELECTED_CODE_INFO_ENABLED = false; export type WebViewMessage = {