Skip to content

Commit

Permalink
[PowerPages] [Create-Site] Implement multi-step site creation with en…
Browse files Browse the repository at this point in the history
…hanced input handling (#1063)

* Enhance CreateSiteCommand to include extension context and add ReadonlyFileSystemProvider for site page previews

* Implement EditableFileSystemProvider for site page editing and update CreateSiteHelper to utilize it

* Integrate CreateSiteCommand into CommandRegistry and update related components for site creation functionality

* Disable copy functionality in EditableFileSystemProvider implementation

* Remove ReadonlyFileSystemProvider implementation

* Add telemetry constant for previewing site pages and refactor related components

* Refactor CommandRegistry and add command registration utility for chat participants

* Add constants for site creation parameters and refactor NL2SiteService to use them

* Refactor CreateSiteCommand and CreateSiteHelper to use structured options and improve readability; add CreateSiteTypes for better type management

* Add error telemetry constant for previewing site pages and handle errors in previewSitePagesContent function

* Rename fileContentMap to _fileContentMap for consistency and clarity in EditableFileSystemProvider

* Implement multi-step input for site creation and register command for user inputs in CreateSiteHelper

* Enhance copilot availability checks and update response structure in ArtemisService

* Add site creation inputs and environment info interfaces; refactor CreateSiteCommand

* Update src/common/chat-participants/powerpages/commands/create-site/CreateSiteTypes.ts

Co-authored-by: Priyanshu Agrawal <[email protected]>

* Update src/common/chat-participants/powerpages/commands/create-site/CreateSiteHelper.ts

Co-authored-by: Priyanshu Agrawal <[email protected]>

* Update src/common/chat-participants/powerpages/commands/create-site/CreateSiteHelper.ts

Co-authored-by: Priyanshu Agrawal <[email protected]>

* Add eslint directives to suppress no-explicit-any warnings in site page handling

---------

Co-authored-by: amitjoshi <[email protected]>
Co-authored-by: Priyanshu Agrawal <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2024
1 parent 46d8cc0 commit c115f29
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 23 deletions.
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;
}
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 {
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

0 comments on commit c115f29

Please sign in to comment.