Skip to content

Commit

Permalink
[Copilot] Show user selected code along with prompt in copilot panel (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
Co-authored-by: tyaginidhi <[email protected]>
  • Loading branch information
3 people authored Oct 9, 2023
1 parent 907e064 commit a590a15
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/common/copilot/IntelligenceApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 25 additions & 10 deletions src/common/copilot/PowerPagesCopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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))
);
}

Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions src/common/copilot/assets/copilotStrings.ts
Original file line number Diff line number Diff line change
@@ -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:'),
}
99 changes: 40 additions & 59 deletions src/common/copilot/assets/scripts/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
let isCopilotEnabled = true;
let isLoggedIn = false;
let apiResponseInProgress = false;

let selectedCode = "";
let copilotStrings = {};


const inputHistory = [];
Expand Down Expand Up @@ -62,7 +63,7 @@

vscode.postMessage({ type: "webViewLoaded" });

function parseCodeBlocks(responseText) {
function parseCodeBlocks(responseText, isUserCode) {
const resultDiv = document.createElement("div");
let codeLineCount = 0;

Expand All @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

}
});

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/common/copilot/assets/styles/copilot.css
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
1 change: 1 addition & 0 deletions src/common/copilot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit a590a15

Please sign in to comment.