Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Copilot] Show user selected code along with prompt in copilot panel #730

Merged
merged 17 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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") {
amitjoshi438 marked this conversation as resolved.
Show resolved Hide resolved
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