Skip to content

Commit

Permalink
[PowerPages][create-site] Preview and Edit Site Page and Command Regi…
Browse files Browse the repository at this point in the history
…stration (#1061)

* 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

* Remove unused getUpdatedPageContent function from CreateSiteHelper to streamline code

* Add ESLint disable comments for any type usage in CreateSiteHelper and CreateSiteTypes

---------

Co-authored-by: amitjoshi <[email protected]>
  • Loading branch information
amitjoshi438 and amitjoshi authored Nov 27, 2024
1 parent c7d4ff9 commit 8fad281
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 24 deletions.
7 changes: 7 additions & 0 deletions src/common/chat-participants/ChatParticipantUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
*/

import * as vscode from 'vscode';
import { Command, CommandRegistry } from './CommandRegistry';

export function createChatParticipant(participantId: string, handler: vscode.ChatRequestHandler): vscode.ChatParticipant {
return vscode.chat.createChatParticipant(participantId, handler);
}

export function registerCommands(commandRegistry: CommandRegistry, commands: { [key: string]: Command }) {
for (const commandName in commands) {
commandRegistry.register(commandName, commands[commandName]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*/

import * as vscode from 'vscode';
import { createChatParticipant } from '../ChatParticipantUtils';
import { createChatParticipant, registerCommands } 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';
import { intelligenceAPIAuthentication } from '../../services/AuthenticationProvider';
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, RESPONSE_AWAITED_MSG, RESPONSE_SCENARIOS, SKIP_CODES, STATER_PROMPTS, WELCOME_MESSAGE, WELCOME_PROMPT } from './PowerPagesChatParticipantConstants';
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 { checkCopilotAvailability, fetchRelatedFiles, getActiveEditorContent } from '../../utilities/Utils';
Expand All @@ -25,10 +25,6 @@ import { oneDSLoggerWrapper } from '../../OneDSLoggerTelemetry/oneDSLoggerWrappe
import { CommandRegistry } from '../CommandRegistry';
import { VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_INVOKED, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_ORG_DETAILS_NOT_FOUND, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_ORG_DETAILS, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_NOT_AVAILABLE_ECS, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SUCCESSFUL_PROMPT, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_WELCOME_PROMPT, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_NO_PROMPT, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_LOCATION_REFERENCED, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_WEBPAGE_RELATED_FILES, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_SCENARIO, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_ERROR, VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_COMMAND_TRIGGERED } from './PowerPagesChatParticipantTelemetryConstants';

// Initialize Command Registry and Register Commands
const commandRegistry = new CommandRegistry();
//Register Commands

export class PowerPagesChatParticipant {
private static instance: PowerPagesChatParticipant | null = null;
private chatParticipant: vscode.ChatParticipant;
Expand Down Expand Up @@ -98,6 +94,10 @@ export class PowerPagesChatParticipant {
this.telemetry.sendTelemetryEvent(VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_INVOKED, { sessionId: this.powerPagesAgentSessionId });
oneDSLoggerWrapper.getLogger().traceInfo(VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_INVOKED, { sessionId: this.powerPagesAgentSessionId });

const commandRegistry = new CommandRegistry();

registerCommands(commandRegistry, POWERPAGES_COMMANDS);

if (!this.isOrgDetailsInitialized) {
stream.progress(PAC_AUTH_INPUT);
await this.initializeOrgDetails();
Expand Down Expand Up @@ -174,7 +174,8 @@ export class PowerPagesChatParticipant {
telemetry: this.telemetry,
orgID: this.orgID,
envID: this.environmentID,
userId: userId
userId: userId,
extensionContext: this.extensionContext
};

return await command.execute(commandRequest, stream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode';
import { ADX_ENTITYFORM, ADX_ENTITYLIST } from '../../copilot/constants';
import { CreateSiteCommand } from './commands/create-site/CreateSiteCommand';

// Constants
export const POWERPAGES_CHAT_PARTICIPANT_ID = 'powerpages';
Expand Down Expand Up @@ -58,3 +59,6 @@ export const NL2PAGE_GENERATING_WEBPAGES = vscode.l10n.t("Generating webpages...
export const NL2PAGE_RESPONSE_FAILED = 'Failed to get page content from NL2Page service';
export const NL2SITE_GENERATING_SITE = vscode.l10n.t("Generating a new Power Pages site...");
export const FAILED_TO_CREATE_SITE = vscode.l10n.t('Failed to create a new Power Pages site. Please try again.');
export const POWERPAGES_COMMANDS = {
'create-site': new CreateSiteCommand()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export const VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED = 'VSCodeExtensionNL2Si
export const VSCODE_EXTENSION_GITHUB_POWER_PAGES_AGENT_COMMAND_TRIGGERED = 'VSCodeExtensionGitHubPowerPagesAgentCommandTriggered';
export const VSCODE_EXTENSION_NL2PAGE_REQUEST = 'VSCodeExtensionNL2PageRequest';
export const VSCODE_EXTENSION_NL2SITE_REQUEST = 'VSCodeExtensionNL2SiteRequest';
export const VSCODE_EXTENSION_PREVIEW_SITE_PAGES = 'VSCodeExtensionPreviewSitePages';
export const VSCODE_EXTENSION_PREVIEW_SITE_PAGES_ERROR = 'VSCodeExtensionPreviewSitePagesError';
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,24 @@ import { VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED} from "../../PowerPagesChat

export class CreateSiteCommand implements Command {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async execute(request: any, stream: vscode.ChatResponseStream): Promise<any> {
const { prompt, intelligenceAPIEndpointInfo, intelligenceApiToken, powerPagesAgentSessionId, telemetry, orgId, envId, userId } = request;
async execute(requestObject: any, stream: vscode.ChatResponseStream): Promise<any> {
const { request, intelligenceAPIEndpointInfo, intelligenceApiToken, powerPagesAgentSessionId, telemetry, orgID, envID, userId, extensionContext } = requestObject;

stream.progress(NL2SITE_GENERATING_SITE);
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const result = await createSite(
intelligenceAPIEndpointInfo.intelligenceEndpoint,
const result = await createSite({
intelligenceEndpoint: intelligenceAPIEndpointInfo.intelligenceEndpoint,
intelligenceApiToken,
prompt,
powerPagesAgentSessionId,
userPrompt: request.prompt,
sessionId: powerPagesAgentSessionId,
stream,
telemetry,
orgId,
envId,
userId
);
orgId: orgID,
envId: envID,
userId,
extensionContext
});
// Process the result

return {
Expand All @@ -38,8 +39,8 @@ export class CreateSiteCommand implements Command {
};
} catch (error) {
stream.markdown(FAILED_TO_CREATE_SITE);
telemetry.sendTelemetryEvent(VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED, { sessionId: powerPagesAgentSessionId, orgId:orgId, envId: envId, userId: userId, error: error as string });
oneDSLoggerWrapper.getLogger().traceError(VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED, error as string, error as Error, { sessionId: powerPagesAgentSessionId, orgId:orgId, envId: envId, userId: userId}, {});
telemetry.sendTelemetryEvent(VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED, { sessionId: powerPagesAgentSessionId, orgId:orgID, envId: envID, userId: userId, error: error as string });
oneDSLoggerWrapper.getLogger().traceError(VSCODE_EXTENSION_CREATE_SITE_COMMAND_FAILED, error as string, error as Error, { sessionId: powerPagesAgentSessionId, orgId:orgID, envId: envID, userId: userId}, {});
return {
metadata: {
command: ''
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

export const EDITABLE_SCHEME = 'editable';
export const ENGLISH = "English";
export const MIN_PAGES = 7;
export const MAX_PAGES = 7;
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,32 @@ import { getNL2PageData } from './Nl2PageService';
import { getNL2SiteData } from './Nl2SiteService';
import { NL2SITE_REQUEST_FAILED, NL2PAGE_GENERATING_WEBPAGES, NL2PAGE_RESPONSE_FAILED } from '../../PowerPagesChatParticipantConstants';
import { oneDSLoggerWrapper } from '../../../../OneDSLoggerTelemetry/oneDSLoggerWrapper';
import { VSCODE_EXTENSION_NL2PAGE_REQUEST, VSCODE_EXTENSION_NL2SITE_REQUEST } from '../../PowerPagesChatParticipantTelemetryConstants';
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';

export const createSite = async (intelligenceEndpoint: string, intelligenceApiToken: string, userPrompt: string, sessionId: string, stream: vscode.ChatResponseStream, telemetry: ITelemetry, orgId: string, envID: string, userId: string) => {
const { siteName, siteDescription } = await fetchSiteAndPageData(intelligenceEndpoint, intelligenceApiToken, userPrompt, sessionId, telemetry, stream, orgId, envID, userId);
export const createSite = async (createSiteOptions: ICreateSiteOptions) => {
const {
intelligenceEndpoint,
intelligenceApiToken,
userPrompt,
sessionId,
stream,
telemetry,
orgId,
envId,
userId,
extensionContext
} = createSiteOptions;

const { siteName, siteDescription, sitePages } = await fetchSiteAndPageData(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});

// TODO: Implement the create site button click handler

return {
siteName,
Expand Down Expand Up @@ -46,3 +68,53 @@ async function fetchSiteAndPageData(intelligenceEndpoint: string, intelligenceAp

return { siteName, sitePagesList, sitePages, siteDescription };
}


function previewSitePagesContent(
options: IPreviewSitePagesContentOptions
): EditableFileSystemProvider {
const {
sitePages,
stream,
extensionContext,
telemetry,
sessionId,
orgId,
envId,
userId
} = options;

try {
const sitePagesContent: { name: string; content: string }[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sitePages.forEach((page: any) => {
sitePagesContent.push({ name: page.metadata.pageTitle, content: page.code });
});

const sitePagesFolder: vscode.ChatResponseFileTree[] = [];
const contentProvider = new EditableFileSystemProvider();
// Register the content provider
extensionContext.subscriptions.push(
vscode.workspace.registerFileSystemProvider(EDITABLE_SCHEME, contentProvider, { isCaseSensitive: true })
);

const baseUri = vscode.Uri.parse(`${EDITABLE_SCHEME}:/`);

sitePagesContent.forEach((page: { name: string; content: string; }) => {
sitePagesFolder.push({ name: page.name + HTML_FILE_EXTENSION });
const pageUri = vscode.Uri.joinPath(baseUri, page.name + HTML_FILE_EXTENSION);
contentProvider.writeFile(pageUri, Buffer.from(page.content, UTF8_ENCODING));
});

telemetry.sendTelemetryEvent(VSCODE_EXTENSION_PREVIEW_SITE_PAGES, { sessionId, orgId, environmentId: envId, userId });

stream.filetree(sitePagesFolder, baseUri);

return contentProvider;
} catch (error) {
telemetry.sendTelemetryEvent(VSCODE_EXTENSION_PREVIEW_SITE_PAGES_ERROR, { sessionId, orgId, environmentId: envId, userId, error: (error as Error).message });
oneDSLoggerWrapper.getLogger().traceError(VSCODE_EXTENSION_PREVIEW_SITE_PAGES_ERROR, (error as Error).message, error as Error, { sessionId, orgId, environmentId: envId, userId }, {});
throw error;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

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

export interface ICreateSiteOptions {
intelligenceEndpoint: string;
intelligenceApiToken: string;
userPrompt: string;
sessionId: string;
stream: vscode.ChatResponseStream;
telemetry: ITelemetry;
orgId: string;
envId: string;
userId: string;
extensionContext: vscode.ExtensionContext;
}

export interface IPreviewSitePagesContentOptions {
// siteName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sitePages: any[];
stream: vscode.ChatResponseStream;
extensionContext: vscode.ExtensionContext;
telemetry: ITelemetry;
sessionId: string;
orgId: string;
envId: string;
userId: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NL2SITE_GENERATE_NEW_SITE, NL2SITE_INVALID_RESPONSE, NL2SITE_SCENARIO}
import {VSCODE_EXTENSION_NL2SITE_REQUEST_FAILED, VSCODE_EXTENSION_NL2SITE_REQUEST_SUCCESS } from "../../PowerPagesChatParticipantTelemetryConstants";
import { getCommonHeaders } from "../../../../services/AuthenticationProvider";
import { oneDSLoggerWrapper } from "../../../../OneDSLoggerTelemetry/oneDSLoggerWrapper";
import { ENGLISH, MAX_PAGES, MIN_PAGES } from "./CreateSiteConstants";

export async function getNL2SiteData(aibEndpoint: string, aibToken: string, userPrompt: string, sessionId: string, telemetry: ITelemetry, orgId: string, envId: string, userId: string) {
const requestBody = {
Expand All @@ -22,8 +23,10 @@ export async function getNL2SiteData(aibEndpoint: string, aibToken: string, user
// "shouldCheckBlockList": false, //TODO: Check if this is needed
"version": "V1",
"information": {
"minPages": 7,
"maxPages": 7
"minPages": MIN_PAGES,
"maxPages": MAX_PAGES,
"language": ENGLISH

}
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,5 @@ export interface IApiRequestParams {

export const VSCODE_EXTENSION_COPILOT_CONTEXT_RELATED_FILES_FETCH_FAILED = "VSCodeExtensionCopilotContextRelatedFilesFetchFailed";
export const ADX_WEBPAGE = 'adx_webpage'
export const HTML_FILE_EXTENSION = '.html';
export const UTF8_ENCODING = 'utf8';
65 changes: 65 additions & 0 deletions src/common/utilities/EditableFileSystemProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

/* eslint-disable @typescript-eslint/no-unused-vars */

import * as vscode from 'vscode';


export class EditableFileSystemProvider implements vscode.FileSystemProvider {
private _fileContentMap: { [key: string]: Uint8Array } = {};
private _onDidChangeEmitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile = this._onDidChangeEmitter.event;

watch(uri: vscode.Uri, options: { readonly recursive: boolean; readonly excludes: readonly string[]; }): vscode.Disposable {
// For simplicity, this implementation does not support file watching.
// eslint-disable-next-line @typescript-eslint/no-empty-function
return new vscode.Disposable(() => {});
}

copy(source: vscode.Uri, destination: vscode.Uri, options: { readonly overwrite: boolean; }): void | Thenable<void> {
// Copy is not supported in this implementation
}

// Read file content
readFile(uri: vscode.Uri): Uint8Array {
const filePath = uri.path;
return this._fileContentMap[filePath] || new Uint8Array();
}

// Write file content
writeFile(uri: vscode.Uri, content: Uint8Array): void {
const filePath = uri.path;
this._fileContentMap[filePath] = content;
this._onDidChangeEmitter.fire([{ type: vscode.FileChangeType.Changed, uri }]);
}

// Other required methods for FileSystemProvider
stat(uri: vscode.Uri): vscode.FileStat {
return { type: vscode.FileType.File, ctime: Date.now(), mtime: Date.now(), size: this._fileContentMap[uri.path]?.length || 0 };
}

readDirectory(uri: vscode.Uri): [string, vscode.FileType][] {
return [];
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
createDirectory(uri: vscode.Uri): void {}

delete(uri: vscode.Uri): void {
// Delete is not supported in this implementation
}

rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { readonly overwrite: boolean; }): void {
// Rename is not supported in this implementation
}

// Method to get file content as string
getFileContent(uri: vscode.Uri): string {
const filePath = uri.path;
const content = this._fileContentMap[filePath];
return content ? Buffer.from(content).toString('utf8') : '';
}
}

0 comments on commit 8fad281

Please sign in to comment.