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

[PowerPages] [Create-Site] Implement multi-step site creation with enhanced input handling #1063

Merged
merged 24 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b5eff91
Enhance CreateSiteCommand to include extension context and add Readon…
Nov 20, 2024
17aaad6
Implement EditableFileSystemProvider for site page editing and update…
Nov 22, 2024
b9d7301
Integrate CreateSiteCommand into CommandRegistry and update related c…
Nov 22, 2024
8b32d09
Disable copy functionality in EditableFileSystemProvider implementation
Nov 22, 2024
2636dcd
Remove ReadonlyFileSystemProvider implementation
Nov 22, 2024
7b26d08
Add telemetry constant for previewing site pages and refactor related…
Nov 22, 2024
66240c4
Refactor CommandRegistry and add command registration utility for cha…
Nov 22, 2024
255c97e
Add constants for site creation parameters and refactor NL2SiteServic…
Nov 22, 2024
7653622
Refactor CreateSiteCommand and CreateSiteHelper to use structured opt…
Nov 25, 2024
537414c
Add error telemetry constant for previewing site pages and handle err…
Nov 26, 2024
0f73785
Rename fileContentMap to _fileContentMap for consistency and clarity …
Nov 26, 2024
ba1dde6
Implement multi-step input for site creation and register command for…
Nov 27, 2024
04aa45e
Merge branch 'main' of https://github.com/microsoft/powerplatform-vsc…
Nov 27, 2024
f921e3b
Enhance copilot availability checks and update response structure in …
Nov 27, 2024
22dc98f
Add site creation inputs and environment info interfaces; refactor Cr…
Nov 27, 2024
32bb589
Add eslint directive for sitePages property in IPreviewSitePagesConte…
Nov 28, 2024
5c67c77
Update src/common/chat-participants/powerpages/commands/create-site/C…
amitjoshi438 Dec 3, 2024
6f56920
Update src/common/chat-participants/powerpages/commands/create-site/C…
amitjoshi438 Dec 3, 2024
403f87a
Update src/common/chat-participants/powerpages/commands/create-site/C…
amitjoshi438 Dec 3, 2024
8fe8147
Merge branch 'main' into users/amitjoshi/copilotSiteCreateQuickPick
amitjoshi438 Dec 3, 2024
fcd3f55
Merge branch 'main' into users/amitjoshi/copilotSiteCreateQuickPick
amitjoshi438 Dec 6, 2024
b0a1e9d
Add eslint directives to suppress no-explicit-any warnings in site pa…
Dec 10, 2024
94ce4cc
Merge branch 'users/amitjoshi/copilotSiteCreateQuickPick' of https://…
Dec 10, 2024
9e3f827
Merge branch 'main' into users/amitjoshi/copilotSiteCreateQuickPick
amitjoshi438 Dec 13, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { intelligenceAPIAuthentication } from '../../services/AuthenticationProv
import { ActiveOrgOutput } from '../../../client/pac/PacTypes';
import { AUTHENTICATION_FAILED_MSG, COPILOT_NOT_AVAILABLE_MSG, COPILOT_NOT_RELEASED_MSG, DISCLAIMER_MESSAGE, INVALID_RESPONSE, NO_PROMPT_MESSAGE, PAC_AUTH_INPUT, PAC_AUTH_NOT_FOUND, POWERPAGES_CHAT_PARTICIPANT_ID, POWERPAGES_COMMANDS, RESPONSE_AWAITED_MSG, RESPONSE_SCENARIOS, SKIP_CODES, STATER_PROMPTS, WELCOME_MESSAGE, WELCOME_PROMPT } from './PowerPagesChatParticipantConstants';
import { ORG_DETAILS_KEY, handleOrgChangeSuccess, initializeOrgDetails } from '../../utilities/OrgHandlerUtils';
import { createAndReferenceLocation, getComponentInfo, getEndpoint, provideChatParticipantFollowups, handleChatParticipantFeedback, createErrorResult, createSuccessResult, removeChatVariables } from './PowerPagesChatParticipantUtils';
import { createAndReferenceLocation, getComponentInfo, getEndpoint, provideChatParticipantFollowups, handleChatParticipantFeedback, createErrorResult, createSuccessResult, removeChatVariables, registerButtonCommands } from './PowerPagesChatParticipantUtils';
import { checkCopilotAvailability, fetchRelatedFiles, getActiveEditorContent } from '../../utilities/Utils';
import { IIntelligenceAPIEndpointInformation } from '../../services/Interfaces';
import { v4 as uuidv4 } from 'uuid';
Expand Down Expand Up @@ -63,6 +63,8 @@ export class PowerPagesChatParticipant {

this._pacWrapper = pacWrapper;

registerButtonCommands();

this._disposables.push(orgChangeEvent(async (orgDetails: ActiveOrgOutput) => {
await this.handleOrgChangeSuccess(orgDetails);
}));
Expand Down Expand Up @@ -131,9 +133,13 @@ export class PowerPagesChatParticipant {
const userId = intelligenceApiAuthResponse.userId;
const intelligenceAPIEndpointInfo = await getEndpoint(this.orgID, this.environmentID, this.telemetry, this.cachedEndpoint, this.powerPagesAgentSessionId);

if (!intelligenceAPIEndpointInfo.intelligenceEndpoint) {
return createErrorResult(COPILOT_NOT_AVAILABLE_MSG, RESPONSE_SCENARIOS.COPILOT_NOT_AVAILABLE, this.orgID);
}

const copilotAvailabilityStatus = checkCopilotAvailability(intelligenceAPIEndpointInfo.intelligenceEndpoint, this.orgID, this.telemetry, this.powerPagesAgentSessionId);

if (!copilotAvailabilityStatus || !intelligenceAPIEndpointInfo.intelligenceEndpoint) {
if (!copilotAvailabilityStatus) {
return createErrorResult(COPILOT_NOT_AVAILABLE_MSG, RESPONSE_SCENARIOS.COPILOT_NOT_AVAILABLE, this.orgID);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { ITelemetry } from "../../OneDSLoggerTelemetry/telemetry/ITelemetry";
import { ArtemisService } from "../../services/ArtemisService";
import { dataverseAuthentication } from "../../services/AuthenticationProvider";
import { IIntelligenceAPIEndpointInformation } from "../../services/Interfaces";
import { EditableFileSystemProvider } from "../../utilities/EditableFileSystemProvider";
import { CREATE_SITE_BTN_CMD } from "./commands/create-site/CreateSiteConstants";
import { collectSiteCreationInputs, getUpdatedPageContent } from "./commands/create-site/CreateSiteHelper";
import { SUPPORTED_ENTITIES, EXPLAIN_CODE_PROMPT, FORM_PROMPT, LIST_PROMPT, STATER_PROMPTS, WEB_API_PROMPT } from "./PowerPagesChatParticipantConstants";
import { VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SCENARIO_FEEDBACK_THUMBSUP, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SCENARIO_FEEDBACK_THUMBSDOWN } from "./PowerPagesChatParticipantTelemetryConstants";
import { IComponentInfo, IPowerPagesChatResult } from "./PowerPagesChatParticipantTypes";
Expand Down Expand Up @@ -127,3 +130,22 @@ export function removeChatVariables(userPrompt: string): string {

return userPrompt.replace(regex, '').trim();
}

export function registerButtonCommands() {
vscode.commands.registerCommand(CREATE_SITE_BTN_CMD, async (siteName: string, sitePages, envList, contentProvider: EditableFileSystemProvider, isCreateSiteInputsReceived) => {
if (!isCreateSiteInputsReceived) {
//Update Page Content will be used for the site creation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sitePages.map((page: any) => {
return {
...page,
code: getUpdatedPageContent(contentProvider, page.metadata.pageTitle)
}
});
const siteCreateInputs = await collectSiteCreationInputs(siteName, envList);
if (siteCreateInputs) {
isCreateSiteInputsReceived = true;
}
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class CreateSiteCommand implements Command {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const result = await createSite({
intelligenceEndpoint: intelligenceAPIEndpointInfo.intelligenceEndpoint,
intelligenceAPIEndpointInfo,
intelligenceApiToken,
userPrompt: request.prompt,
sessionId: powerPagesAgentSessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ export const EDITABLE_SCHEME = 'editable';
export const ENGLISH = "English";
export const MIN_PAGES = 7;
export const MAX_PAGES = 7;
export const SITE_CREATE_INPUTS = 'New Power Pages Site';
export const ENVIRONMENT_FOR_SITE_CREATION = 'Select Environment for Site Creation';
export const SITE_NAME = 'Enter Site Name';
export const SITE_NAME_REQUIRED = 'Site Name is required';
export const CREATE_SITE_BTN_CMD = 'create-site-inputs';
export const CREATE_SITE_BTN_TITLE = 'Create Site';
export const CREATE_SITE_BTN_TOOLTIP = 'Create a new Power Pages site';
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { NL2SITE_REQUEST_FAILED, NL2PAGE_GENERATING_WEBPAGES, NL2PAGE_RESPONSE_F
import { oneDSLoggerWrapper } from '../../../../OneDSLoggerTelemetry/oneDSLoggerWrapper';
import { VSCODE_EXTENSION_NL2PAGE_REQUEST, VSCODE_EXTENSION_NL2SITE_REQUEST, VSCODE_EXTENSION_PREVIEW_SITE_PAGES, VSCODE_EXTENSION_PREVIEW_SITE_PAGES_ERROR } from '../../PowerPagesChatParticipantTelemetryConstants';
import { EditableFileSystemProvider } from '../../../../utilities/EditableFileSystemProvider';
import { HTML_FILE_EXTENSION, UTF8_ENCODING } from '../../../../constants';
import { EDITABLE_SCHEME } from './CreateSiteConstants';
import { ICreateSiteOptions, IPreviewSitePagesContentOptions } from './CreateSiteTypes';
import { HTML_FILE_EXTENSION, IEnvInfo, UTF8_ENCODING } from '../../../../constants';
import { CREATE_SITE_BTN_CMD, CREATE_SITE_BTN_TITLE, CREATE_SITE_BTN_TOOLTIP, EDITABLE_SCHEME, ENVIRONMENT_FOR_SITE_CREATION, SITE_CREATE_INPUTS, SITE_NAME, SITE_NAME_REQUIRED } from './CreateSiteConstants';
import { ICreateSiteOptions, IPreviewSitePagesContentOptions, ISiteInputState } from './CreateSiteTypes';
import { MultiStepInput } from '../../../../utilities/MultiStepInput';
import { getEnvList } from '../../../../utilities/Utils';

export const createSite = async (createSiteOptions: ICreateSiteOptions) => {
const {
intelligenceEndpoint,
intelligenceAPIEndpointInfo,
intelligenceApiToken,
userPrompt,
sessionId,
Expand All @@ -29,12 +31,22 @@ export const createSite = async (createSiteOptions: ICreateSiteOptions) => {
extensionContext
} = createSiteOptions;

const { siteName, siteDescription, sitePages } = await fetchSiteAndPageData(intelligenceEndpoint, intelligenceApiToken, userPrompt, sessionId, telemetry, stream, orgId, envId, userId);
if (!intelligenceAPIEndpointInfo.intelligenceEndpoint) {
return;
amitjoshi438 marked this conversation as resolved.
Show resolved Hide resolved
}
const { siteName, siteDescription, sitePages } = await fetchSiteAndPageData(intelligenceAPIEndpointInfo.intelligenceEndpoint, intelligenceApiToken, userPrompt, sessionId, telemetry, stream, orgId, envId, userId);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const contentProvider = previewSitePagesContent({sitePages, stream, extensionContext, telemetry, sessionId, orgId, envId, userId});
const contentProvider = previewSitePagesContent({ sitePages, stream, extensionContext, telemetry, sessionId, orgId, envId, userId });

const envList = await getEnvList(telemetry, intelligenceAPIEndpointInfo.endpointStamp);

// TODO: Implement the create site button click handler
stream.button({
command: CREATE_SITE_BTN_CMD,
title: CREATE_SITE_BTN_TITLE,
tooltip: CREATE_SITE_BTN_TOOLTIP,
arguments: [siteName, envList, contentProvider, false],
});

return {
siteName,
Expand Down Expand Up @@ -117,4 +129,63 @@ function previewSitePagesContent(
throw error;
}
}
// Function to get updated content
export function getUpdatedPageContent(contentProvider: EditableFileSystemProvider, pageName: string): string {
const pageUri = vscode.Uri.parse(`${EDITABLE_SCHEME}:/${pageName}${HTML_FILE_EXTENSION}`);
return contentProvider.getFileContent(pageUri);
}

export async function collectSiteCreationInputs(siteName: string, envList: IEnvInfo[]) {
const envNames: vscode.QuickPickItem[] = envList.map((env: IEnvInfo) => {
return {
label: env.envDisplayName,
description: env.orgUrl,
};
});

const title = vscode.l10n.t(SITE_CREATE_INPUTS);

async function collectInputs() {
const state = {} as Partial<ISiteInputState>;
await MultiStepInput.run((input) => selectEnvName(input, state));
return state as ISiteInputState;
}

async function selectEnvName(
input: MultiStepInput,
state: Partial<ISiteInputState>
) {
const pick = await input.showQuickPick({
title,
step: 1,
totalSteps: 2,
placeholder: vscode.l10n.t(ENVIRONMENT_FOR_SITE_CREATION),
items: envNames,
activeItem:
typeof state.envName !== "string"
? state.envName
: undefined,
});
state.envName = pick.label;
state.OrgUrl = pick.description;
return (input: MultiStepInput) => inputSiteName(input, state);
}

async function inputSiteName(
input: MultiStepInput,
state: Partial<ISiteInputState>
) {
state.siteName = await input.showInputBox({
title,
step: 2,
totalSteps: 2,
value: state.siteName || siteName,
placeholder: vscode.l10n.t(SITE_NAME),
validate: async (value) => (value ? undefined : vscode.l10n.t(SITE_NAME_REQUIRED)),
});
}

const siteInputState = await collectInputs();
// Return the collected site creation inputs including site name, environment name, and domain name
return siteInputState;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

import { ITelemetry } from "../../../../OneDSLoggerTelemetry/telemetry/ITelemetry";
import * as vscode from 'vscode';
import { IIntelligenceAPIEndpointInformation } from "../../../../services/Interfaces";

export interface ICreateSiteOptions {
intelligenceEndpoint: string;
intelligenceAPIEndpointInfo: IIntelligenceAPIEndpointInformation;
intelligenceApiToken: string;
userPrompt: string;
sessionId: string;
Expand All @@ -31,3 +32,13 @@ export interface IPreviewSitePagesContentOptions {
envId: string;
userId: string;
}

export interface ISiteInputState {
siteName: string;
envName: string;
orgUrl: string;
domainName: string;
title: string;
step: number;
totalSteps: number;
}
5 changes: 5 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export interface IApiRequestParams {
relatedFiles?: IRelatedFiles[];
}

export interface IEnvInfo {
orgUrl: string;
envDisplayName: string;
}

export const VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED = "VSCodeExtensionCopilotContextRelatedFilesFetchFailed";
export const ADX_WEBPAGE = 'adx_webpage'
export const HTML_FILE_EXTENSION = '.html';
Expand Down
4 changes: 4 additions & 0 deletions src/common/copilot/PowerPagesCopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider {
sendTelemetryEvent(this.telemetry, { eventName: CopilotOrgChangedEvent, copilotSessionId: sessionID, orgId: orgID });

const intelligenceAPIEndpointInfo = await ArtemisService.getIntelligenceEndpoint(orgID, this.telemetry, sessionID, environmentId);
if (!intelligenceAPIEndpointInfo.intelligenceEndpoint) {
this.sendMessageToWebview({ type: 'Unavailable' });
return;
}
this.aibEndpoint = intelligenceAPIEndpointInfo.intelligenceEndpoint;
this.geoName = intelligenceAPIEndpointInfo.geoName;
this.crossGeoDataMovementEnabledPPACFlag = intelligenceAPIEndpointInfo.crossGeoDataMovementEnabledPPACFlag;
Expand Down
6 changes: 4 additions & 2 deletions src/common/services/ArtemisService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export class ArtemisService {
if (artemisResponse === null) {
return { intelligenceEndpoint: null, geoName: null, crossGeoDataMovementEnabledPPACFlag: false };
}
const { geoName, environment, clusterNumber } = artemisResponse.response as unknown as IArtemisAPIOrgResponse;

const endpointStamp = artemisResponse.stamp;
const { geoName, environment, clusterNumber } = artemisResponse.response as IArtemisAPIOrgResponse;
sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId });

const crossGeoDataMovementEnabledPPACFlag = await BAPService.getCrossGeoCopilotDataMovementEnabledFlag(artemisResponse.stamp, telemetry, environmentId);
Expand All @@ -38,7 +40,7 @@ export class ArtemisService {

const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat`

return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag };
return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag, endpointStamp: endpointStamp };
}

// Function to fetch Artemis response
Expand Down
8 changes: 2 additions & 6 deletions src/common/services/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,11 @@ export interface IArtemisAPIOrgResponse {
clusterType: string,
}

export interface IArtemisServiceResponse {
amitjoshi438 marked this conversation as resolved.
Show resolved Hide resolved
stamp: ServiceEndpointCategory;
response: IArtemisAPIOrgResponse;
}

export interface IIntelligenceAPIEndpointInformation {
intelligenceEndpoint: string | null,
geoName: string | null,
crossGeoDataMovementEnabledPPACFlag: boolean
crossGeoDataMovementEnabledPPACFlag: boolean,
endpointStamp?: ServiceEndpointCategory,
}

export interface IWebsiteDetails {
Expand Down
11 changes: 7 additions & 4 deletions src/common/utilities/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from "vscode";
import { componentTypeSchema, EXTENSION_ID, EXTENSION_NAME, IRelatedFiles, relatedFilesSchema, SETTINGS_EXPERIMENTAL_STORE_NAME, VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED } from "../constants";
import { componentTypeSchema, EXTENSION_ID, EXTENSION_NAME, IEnvInfo, IRelatedFiles, relatedFilesSchema, SETTINGS_EXPERIMENTAL_STORE_NAME, VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED } from "../constants";
import { CUSTOM_TELEMETRY_FOR_POWER_PAGES_SETTING_NAME } from "../OneDSLoggerTelemetry/telemetryConstants";
import { COPILOT_UNAVAILABLE, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap } from "../copilot/constants";
import { IActiveFileData } from "../copilot/model";
Expand Down Expand Up @@ -325,8 +325,11 @@ export function getECSOrgLocationValue(clusterName: string, clusterNumber: strin
}

//API call to get env list for an org
export async function getEnvList(telemetry: ITelemetry, endpointStamp: ServiceEndpointCategory): Promise<{ envId: string, envDisplayName: string }[]> {
const envInfo: { envId: string, envDisplayName: string }[] = [];
export async function getEnvList(telemetry: ITelemetry, endpointStamp: ServiceEndpointCategory | undefined): Promise<IEnvInfo[]> {
if(!endpointStamp) {
return [];
}
const envInfo: IEnvInfo[] = [];
try {
const bapAuthToken = await bapServiceAuthentication(telemetry, true);
const bapEndpoint = getBAPEndpoint(endpointStamp, telemetry);
Expand All @@ -344,7 +347,7 @@ export async function getEnvList(telemetry: ITelemetry, endpointStamp: ServiceEn
// eslint-disable-next-line @typescript-eslint/no-explicit-any
envListJson.value.forEach((env: any) => {
envInfo.push({
envId: env.properties.linkedEnvironmentMetadata.instanceUrl,
orgUrl: env.properties.linkedEnvironmentMetadata.instanceUrl,
envDisplayName: env.properties.displayName
});
});
Expand Down
Loading