diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index f41bfd9e..afa8ac2c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -254,4 +254,4 @@ "The {0} represents profile's Azure Cloud Instances" ] } -} \ No newline at end of file +} diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf index 69c3dcd4..3145b496 100644 --- a/loc/translations-export/vscode-powerplatform.xlf +++ b/loc/translations-export/vscode-powerplatform.xlf @@ -318,6 +318,10 @@ The {3} represents Dataverse Environment's Organization ID (GUID) Please enter a name for the webpage. + + Please provide a prompt to get started. + You can get help with writing code for Power Pages sites in HTML, CSS, and JS languages. + Power Pages Copilot is now connected to the environment: {0} : {1} {0} represents the environment name diff --git a/src/common/chat-participants/powerpages/PowerPagesChatParticipant.ts b/src/common/chat-participants/powerpages/PowerPagesChatParticipant.ts index 55da4726..4f4eead9 100644 --- a/src/common/chat-participants/powerpages/PowerPagesChatParticipant.ts +++ b/src/common/chat-participants/powerpages/PowerPagesChatParticipant.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import { createChatParticipant } from '../ChatParticipantUtils'; import { IComponentInfo, IPowerPagesChatResult } from './PowerPagesChatParticipantTypes'; +import { ITelemetry } from "../../OneDSLoggerTelemetry/telemetry/ITelemetry"; import TelemetryReporter from '@vscode/extension-telemetry'; import { sendApiRequest } from '../../copilot/IntelligenceApiService'; import { PacWrapper } from '../../../client/pac/PacWrapper'; @@ -13,11 +14,10 @@ import { intelligenceAPIAuthentication } from '../../services/AuthenticationProv import { ActiveOrgOutput } from '../../../client/pac/PacTypes'; import { AUTHENTICATION_FAILED_MSG, COPILOT_NOT_AVAILABLE_MSG, DISCLAIMER_MESSAGE, NO_PROMPT_MESSAGE, PAC_AUTH_NOT_FOUND, POWERPAGES_CHAT_PARTICIPANT_ID, RESPONSE_AWAITED_MSG, SKIP_CODES, STATER_PROMPTS, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_INVOKED, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_ORG_DETAILS, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_ORG_DETAILS_NOT_FOUND, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SCENARIO, WELCOME_MESSAGE, WELCOME_PROMPT } from './PowerPagesChatParticipantConstants'; import { ORG_DETAILS_KEY, handleOrgChangeSuccess, initializeOrgDetails } from '../../utilities/OrgHandlerUtils'; -import { getComponentInfo, getEndpoint, provideChatParticipantFollowups } from './PowerPagesChatParticipantUtils'; +import { createAndReferenceLocation, getComponentInfo, getEndpoint, provideChatParticipantFollowups } from './PowerPagesChatParticipantUtils'; import { checkCopilotAvailability, getActiveEditorContent } from '../../utilities/Utils'; import { IIntelligenceAPIEndpointInformation } from '../../services/Interfaces'; import { v4 as uuidv4 } from 'uuid'; -import { ITelemetry } from '../../OneDSLoggerTelemetry/telemetry/ITelemetry'; import { orgChangeErrorEvent, orgChangeEvent } from '../../../client/OrgChangeNotifier'; export class PowerPagesChatParticipant { @@ -131,7 +131,7 @@ export class PowerPagesChatParticipant { if (userPrompt === WELCOME_PROMPT) { stream.markdown(WELCOME_MESSAGE); - return{ + return { metadata: { command: STATER_PROMPTS } @@ -139,9 +139,7 @@ export class PowerPagesChatParticipant { } if (!userPrompt) { - stream.markdown(NO_PROMPT_MESSAGE); - return { metadata: { command: STATER_PROMPTS @@ -149,23 +147,31 @@ export class PowerPagesChatParticipant { }; } + const { activeFileContent, activeFileUri, startLine, endLine, activeFileParams } = getActiveEditorContent(); + + const location = activeFileUri ? createAndReferenceLocation(activeFileUri, startLine, endLine) : undefined; + + if (location) { + stream.reference(location); + } + if (request.command) { //TODO: Handle command scenarios } else { - const { activeFileParams } = getActiveEditorContent(); - const { componentInfo, entityName }: IComponentInfo = await getComponentInfo(this.telemetry, this.orgUrl, activeFileParams, this.powerPagesAgentSessionId); - const llmResponse = await sendApiRequest([{ displayText: userPrompt, code: '' }], activeFileParams, this.orgID, intelligenceApiToken, this.powerPagesAgentSessionId, entityName, componentInfo, this.telemetry, intelligenceAPIEndpointInfo.intelligenceEndpoint, intelligenceAPIEndpointInfo.geoName, intelligenceAPIEndpointInfo.crossGeoDataMovementEnabledPPACFlag); + const llmResponse = await sendApiRequest([{ displayText: userPrompt, code: activeFileContent }], activeFileParams, this.orgID, intelligenceApiToken, this.powerPagesAgentSessionId, entityName, componentInfo, this.telemetry, intelligenceAPIEndpointInfo.intelligenceEndpoint, intelligenceAPIEndpointInfo.geoName, intelligenceAPIEndpointInfo.crossGeoDataMovementEnabledPPACFlag); const scenario = llmResponse.length > 1 ? llmResponse[llmResponse.length - 1] : llmResponse[0].displayText; this.telemetry.sendTelemetryEvent(VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SCENARIO, { scenario: scenario, sessionId: this.powerPagesAgentSessionId, orgId: this.orgID, environmentId: this.environmentID }) llmResponse.forEach((response: { displayText: string | vscode.MarkdownString; code: string; }) => { + if (response.displayText) { stream.markdown(response.displayText); + stream.markdown('\n'); } if (response.code && !SKIP_CODES.includes(response.code)) { stream.markdown('\n```javascript\n' + response.code + '\n```'); diff --git a/src/common/chat-participants/powerpages/PowerPagesChatParticipantUtils.ts b/src/common/chat-participants/powerpages/PowerPagesChatParticipantUtils.ts index 94ff0628..570f73b3 100644 --- a/src/common/chat-participants/powerpages/PowerPagesChatParticipantUtils.ts +++ b/src/common/chat-participants/powerpages/PowerPagesChatParticipantUtils.ts @@ -57,6 +57,15 @@ export function isEntityInSupportedList(entity: string): boolean { return SUPPORTED_ENTITIES.includes(entity); } +export function createAndReferenceLocation(activeFileUri: vscode.Uri, startLine: number, endLine: number): vscode.Location { + + const positionStart = new vscode.Position(startLine, 0), + positionEnd = new vscode.Position(endLine, 0), + activeFileRange = new vscode.Range(positionStart, positionEnd), + location = new vscode.Location(activeFileUri, activeFileRange); + + return location; +} // eslint-disable-next-line @typescript-eslint/no-unused-vars export function provideChatParticipantFollowups(result: IPowerPagesChatResult, _context: vscode.ChatContext, _token: vscode.CancellationToken) { if (result.metadata.command === STATER_PROMPTS) { @@ -68,3 +77,4 @@ export function provideChatParticipantFollowups(result: IPowerPagesChatResult, _ ]; } } + diff --git a/src/common/copilot/IntelligenceApiService.ts b/src/common/copilot/IntelligenceApiService.ts index 555bea75..fa0a4214 100644 --- a/src/common/copilot/IntelligenceApiService.ts +++ b/src/common/copilot/IntelligenceApiService.ts @@ -56,7 +56,6 @@ export async function sendApiRequest(userPrompt: UserPrompt[], activeFileParams: } } - const requestInit: RequestInit = { method: "POST", headers: { diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index e225848c..3b15a33b 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -11,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid' import { PacWrapper } from "../../client/pac/PacWrapper"; import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry"; import { ADX_ENTITYFORM, ADX_ENTITYLIST, AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_IN_POWERPAGES, COPILOT_UNAVAILABLE, CopilotStylePathSegments, EXPLAIN_CODE, SELECTED_CODE_INFO, SELECTED_CODE_INFO_ENABLED, THUMBS_DOWN, THUMBS_UP, UserPrompt, WebViewMessage, sendIconSvg } from "./constants"; -import { IActiveFileParams, IOrgInfo } from './model'; +import { IOrgInfo } from './model'; import { checkCopilotAvailability, escapeDollarSign, getActiveEditorContent, getNonce, getSelectedCode, getSelectedCodeLineRange, getUserName, openWalkthrough, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../utilities/Utils"; import { CESUserFeedback } from "./user-feedback/CESSurvey"; import { ActiveOrgOutput } from "../../client/pac/PacTypes"; @@ -236,8 +236,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { sendTelemetryEvent(this.telemetry, { eventName: CopilotUserPromptedEvent, copilotSessionId: sessionID, aibEndpoint: this.aibEndpoint ?? '', orgId: orgID, isSuggestedPrompt: String(data.value.isSuggestedPrompt), crossGeoDataMovementEnabledPPACFlag: this.crossGeoDataMovementEnabledPPACFlag }); //TODO: Add active Editor info orgID ? (async () => { - const { activeFileParams } = getActiveEditorContent(); - await this.authenticateAndSendAPIRequest(data.value.userPrompt, activeFileParams, orgID, this.telemetry); + await this.authenticateAndSendAPIRequest(data.value.userPrompt, orgID, this.telemetry); })() : (() => { this.sendMessageToWebview({ type: 'apiResponse', value: AuthProfileNotFound }); @@ -359,7 +358,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } } - private async authenticateAndSendAPIRequest(data: UserPrompt[], activeFileParams: IActiveFileParams, orgID: string, telemetry: ITelemetry) { + private async authenticateAndSendAPIRequest(data: UserPrompt[], orgID: string, telemetry: ITelemetry) { return intelligenceAPIAuthentication(telemetry, sessionID, orgID) .then(async ({ accessToken, user, userId }) => { intelligenceApiToken = accessToken; @@ -368,6 +367,8 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { this.sendMessageToWebview({ type: 'userName', value: userName }); + const { activeFileContent, activeFileParams } = getActiveEditorContent(); + let metadataInfo = { entityName: '', formName: '' }; let componentInfo: string[] = []; @@ -385,7 +386,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { } } - return sendApiRequest(data, activeFileParams, orgID, intelligenceApiToken, sessionID, metadataInfo.entityName, componentInfo, telemetry, this.aibEndpoint, this.geoName, this.crossGeoDataMovementEnabledPPACFlag); + return sendApiRequest([{ displayText: data[0].displayText, code: activeFileContent }], activeFileParams, orgID, intelligenceApiToken, sessionID, metadataInfo.entityName, componentInfo, telemetry, this.aibEndpoint, this.geoName, this.crossGeoDataMovementEnabledPPACFlag); }) .then(apiResponse => { this.sendMessageToWebview({ type: 'apiResponse', value: apiResponse }); diff --git a/src/common/copilot/model.ts b/src/common/copilot/model.ts index 5cd25e6e..df8e6be0 100644 --- a/src/common/copilot/model.ts +++ b/src/common/copilot/model.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ +import * as vscode from 'vscode'; + export interface IFeedbackData { TenantId: string; Geo: string; @@ -19,7 +21,10 @@ export interface IActiveFileParams { export interface IActiveFileData { activeFileParams: IActiveFileParams; - activeFileContent: string + activeFileContent: string; + activeFileUri: vscode.Uri | undefined; + startLine: number; + endLine: number; } export interface IOrgInfo { diff --git a/src/common/utilities/Utils.ts b/src/common/utilities/Utils.ts index c502ee27..402d3004 100644 --- a/src/common/utilities/Utils.ts +++ b/src/common/utilities/Utils.ts @@ -7,7 +7,7 @@ import * as vscode from "vscode"; import { EXTENSION_ID, EXTENSION_NAME, SETTINGS_EXPERIMENTAL_STORE_NAME } from "../constants"; import { CUSTOM_TELEMETRY_FOR_POWER_PAGES_SETTING_NAME } from "../OneDSLoggerTelemetry/telemetryConstants"; import { COPILOT_UNAVAILABLE, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap } from "../copilot/constants"; -import { IActiveFileData, IActiveFileParams } from "../copilot/model"; +import { IActiveFileData } from "../copilot/model"; import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry"; import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry"; import { getDisabledOrgList, getDisabledTenantList } from "../copilot/utils/copilotUtil"; @@ -141,28 +141,50 @@ export function getUserAgent(): string { export function getActiveEditorContent(): IActiveFileData { const activeEditor = vscode.window.activeTextEditor; - const activeFileData: IActiveFileData = { - activeFileContent: '', - activeFileParams: { - dataverseEntity: '', - entityField: '', - fieldType: '' - } as IActiveFileParams - }; - if (activeEditor) { - const document = activeEditor.document; - const fileName = document.fileName; - const relativeFileName = vscode.workspace.asRelativePath(fileName); - - const activeFileParams: string[] = getLastThreePartsOfFileName(relativeFileName); - activeFileData.activeFileContent = document.getText(); - activeFileData.activeFileParams.dataverseEntity = DataverseEntityNameMap.get(activeFileParams[0]) || ""; - activeFileData.activeFileParams.entityField = EntityFieldMap.get(activeFileParams[1]) || ""; - activeFileData.activeFileParams.fieldType = FieldTypeMap.get(activeFileParams[2]) || ""; + if (!activeEditor) { + return { activeFileContent: '', startLine: 0, endLine: 0, activeFileUri: undefined, activeFileParams: { dataverseEntity: '', entityField: '', fieldType: '' } }; } - return activeFileData; + const document = activeEditor.document; + const fileName = document.fileName; + const relativeFileName = vscode.workspace.asRelativePath(fileName); + const activeFileUri = document.uri; + const activeFileParams: string[] = getLastThreePartsOfFileName(relativeFileName); + + let activeFileContent = document.getText(); + let startLine = 0; + let endLine = document.lineCount; + + const selectedCode = getSelectedCode(activeEditor); + const selectedCodeLineRange = getSelectedCodeLineRange(activeEditor); + + if (selectedCode.length > 0) { + activeFileContent = selectedCode; + startLine = selectedCodeLineRange.start; + endLine = selectedCodeLineRange.end; + } + /** + * Uncomment the below code to pass the visible code to the copilot based on the token limit. + */ + //else if (document.getText().length > 100) { // Define the token limit for context passing + // const { code, startLine: visibleStart, endLine: visibleEnd } = getVisibleCode(activeEditor); + // activeFileContent = code; + // startLine = visibleStart; + // endLine = visibleEnd; + // } + + return { + activeFileContent, + startLine, + endLine, + activeFileUri, + activeFileParams: { + dataverseEntity: DataverseEntityNameMap.get(activeFileParams[0]) || "", + entityField: EntityFieldMap.get(activeFileParams[1]) || "", + fieldType: FieldTypeMap.get(activeFileParams[2]) || "" + } + }; } export function checkCopilotAvailability( @@ -186,3 +208,14 @@ export function checkCopilotAvailability( return true; } } + +export function getVisibleCode(editor: vscode.TextEditor): { code: string; startLine: number; endLine: number; } { + const visibleRanges = editor.visibleRanges; + const visibleCode = visibleRanges.map(range => editor.document.getText(range)).join('\n'); + const firstVisibleRange = visibleRanges[0]; + return { + code: visibleCode, + startLine: firstVisibleRange.start.line, + endLine: firstVisibleRange.end.line + }; +}