diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 470274f3..99b6dfc2 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -522,6 +522,7 @@ "panel.tagPicker.inputPlaceholder.empty": "Pick your {0}", "panel.tagPicker.inputPlaceholder.disabled": "You have reached the limit of {0}", "panel.tagPicker.ai.suggest": "Use Front Matter AI to suggest {0}", + "panel.tagPicker.copilot.suggest": "Use GitHub Copilot to suggest {0}", "panel.tagPicker.ai.generating": "Generating suggestions...", "panel.tagPicker.limit": "Max.: {0}", "panel.tagPicker.unkown": "Add the unknown tag", @@ -705,9 +706,11 @@ "helpers.questions.contentTitle.aiInput.placeholder": "What would you like to write about?", "helpers.questions.contentTitle.aiInput.quickPick.title.separator": "your title/description", "helpers.questions.contentTitle.aiInput.quickPick.ai.separator": "AI generated title", + "helpers.questions.contentTitle.aiInput.quickPick.copilot.separator": "GitHub Copilot suggestions", "helpers.questions.contentTitle.aiInput.select.title": "Select a title", "helpers.questions.contentTitle.aiInput.select.placeholder": "Select a title for your content", "helpers.questions.contentTitle.aiInput.failed": "Failed fetching the AI title. Please try to use your own title or try again later.", + "helpers.questions.contentTitle.copilotInput.failed": "Failed fetching the GitHub Copilot title suggestions. Please try to use your own title or try again later.", "helpers.questions.contentTitle.aiInput.warning": "You did not specify a title for your content.", "helpers.questions.contentTitle.titleInput.title": "Content title", "helpers.questions.contentTitle.titleInput.prompt": "What would you like to use as a title for the content to create?", @@ -781,6 +784,8 @@ "listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "No active editor", "listeners.panel.taxonomyListener.aiSuggestTaxonomy.noData.error": "No article data", + "services.copilot.getChatResponse.error": "Failed to get a response from the GitHub Copilot.", + "services.modeSwitch.switchMode.quickPick.placeholder": "Select the mode you want to use", "services.modeSwitch.switchMode.quickPick.title": "{0}: Mode selection", "services.modeSwitch.setText.mode": "Mode: {0}", diff --git a/package.json b/package.json index c072fed0..3fb2e2c4 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "l10n": "./l10n", "categories": [ + "AI", "Other" ], "keywords": [ diff --git a/src/helpers/Questions.ts b/src/helpers/Questions.ts index 440d9c32..c11a6969 100644 --- a/src/helpers/Questions.ts +++ b/src/helpers/Questions.ts @@ -9,6 +9,7 @@ import { SponsorAi } from '../services/SponsorAI'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; import { ContentFolder } from '../models'; +import { Copilot } from '../services/Copilot'; interface FolderQuickPickItem extends QuickPickItem { path: string; @@ -41,11 +42,12 @@ export class Questions { public static async ContentTitle(showWarning: boolean = true): Promise { const aiEnabled = Settings.get(SETTING_SPONSORS_AI_ENABLED); let title: string | undefined = ''; + const isCopilotInstalled = await Copilot.isInstalled(); - if (aiEnabled) { - const githubAuth = await authentication.getSession('github', ['read:user'], { silent: true }); + let aiTitles: string[] | undefined; - if (githubAuth && githubAuth.account.label) { + if (aiEnabled || isCopilotInstalled) { + if (isCopilotInstalled) { title = await window.showInputBox({ title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputTitle), prompt: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPrompt), @@ -55,54 +57,81 @@ export class Questions { if (title) { try { - const aiTitles = await SponsorAi.getTitles(githubAuth.accessToken, title); - - if (aiTitles && aiTitles.length > 0) { - const options: QuickPickItem[] = [ - { - label: `✏️ ${l10n.t( - LocalizationKey.helpersQuestionsContentTitleAiInputQuickPickTitleSeparator - )}`, - kind: QuickPickItemKind.Separator - }, - { - label: title - }, - { - label: `🤖 ${l10n.t( - LocalizationKey.helpersQuestionsContentTitleAiInputQuickPickAiSeparator - )}`, - kind: QuickPickItemKind.Separator - }, - ...aiTitles.map((d: string) => ({ - label: d - })) - ]; - - const selectedTitle = await window.showQuickPick(options, { - title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputSelectTitle), - placeHolder: l10n.t( - LocalizationKey.helpersQuestionsContentTitleAiInputSelectPlaceholder - ), - ignoreFocusOut: true - }); - - if (selectedTitle) { - title = selectedTitle.label; - } else if (!selectedTitle) { - // Reset the title, so the user can enter their own title - title = undefined; - } - } + aiTitles = await Copilot.suggestTitles(title); } catch (e) { Logger.error((e as Error).message); - Notifications.error(l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputFailed)); + Notifications.error( + l10n.t(LocalizationKey.helpersQuestionsContentTitleCopilotInputFailed) + ); title = undefined; } - } else if (!title && showWarning) { - Notifications.warning(l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputWarning)); - return; } + } else { + const githubAuth = await authentication.getSession('github', ['read:user'], { + silent: true + }); + + if (githubAuth && githubAuth.account.label) { + title = await window.showInputBox({ + title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputTitle), + prompt: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPrompt), + placeHolder: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPlaceholder), + ignoreFocusOut: true + }); + + if (title) { + try { + aiTitles = await SponsorAi.getTitles(githubAuth.accessToken, title); + } catch (e) { + Logger.error((e as Error).message); + Notifications.error( + l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputFailed) + ); + title = undefined; + } + } + } + } + + if (title && aiTitles && aiTitles.length > 0) { + const options: QuickPickItem[] = [ + { + label: `✏️ ${l10n.t( + LocalizationKey.helpersQuestionsContentTitleAiInputQuickPickTitleSeparator + )}`, + kind: QuickPickItemKind.Separator + }, + { + label: title + }, + { + label: `🤖 ${l10n.t( + isCopilotInstalled + ? LocalizationKey.helpersQuestionsContentTitleAiInputQuickPickCopilotSeparator + : LocalizationKey.helpersQuestionsContentTitleAiInputQuickPickAiSeparator + )}`, + kind: QuickPickItemKind.Separator + }, + ...aiTitles.map((d: string) => ({ + label: d + })) + ]; + + const selectedTitle = await window.showQuickPick(options, { + title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputSelectTitle), + placeHolder: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputSelectPlaceholder), + ignoreFocusOut: true + }); + + if (selectedTitle) { + title = selectedTitle.label; + } else if (!selectedTitle) { + // Reset the title, so the user can enter their own title + title = undefined; + } + } else if (!title && showWarning) { + Notifications.warning(l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputWarning)); + return; } } diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index 78a2cafb..a3eb87d8 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -6,13 +6,9 @@ import { Command } from '../../panelWebView/Command'; import { CommandToCode } from '../../panelWebView/CommandToCode'; import { BaseListener } from './BaseListener'; import { - CancellationTokenSource, - LanguageModelChatMessage, - LanguageModelChatResponse, Uri, authentication, commands, - lm, window } from 'vscode'; import { @@ -34,7 +30,6 @@ import { SETTING_DATE_FORMAT, SETTING_GLOBAL_ACTIVE_MODE, SETTING_GLOBAL_MODES, - SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from '../../constants'; @@ -55,6 +50,7 @@ import { Terminal } from '../../services'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../localization'; import { parse } from 'path'; +import { Copilot } from '../../services/Copilot'; const FILE_LIMIT = 10; @@ -114,13 +110,19 @@ export class DataListener extends BaseListener { case CommandToCode.aiSuggestDescription: this.aiSuggestTaxonomy(msg.command, msg.requestId); break; - case CommandToCode.copilotDescription: - this.copilotSuggestion(msg.command, msg.requestId); + case CommandToCode.copilotSuggestDescription: + this.copilotSuggestDescription(msg.command, msg.requestId); break; } } - private static async copilotSuggestion(command: string, requestId?: string) { + /** + * Executes a Copilot suggestion command. + * @param command - The command to execute. + * @param requestId - The optional request ID. + * @returns A Promise that resolves when the suggestion command is executed. + */ + private static async copilotSuggestDescription(command: string, requestId?: string) { if (!command || !requestId) { return; } @@ -138,63 +140,32 @@ export class DataListener extends BaseListener { const extPath = Extension.getInstance().extensionPath; const panel = PanelProvider.getInstance(extPath); - const [model] = await lm.selectChatModels({ - vendor: 'copilot', - // family: 'gpt-4' - }); + const titleField = (Settings.get(SETTING_SEO_TITLE_FIELD) as string) || DefaultFields.Title; + const description = await Copilot.suggestDescription( + articleDetails.data[titleField], + articleDetails.content + ); - // TODO: Create settings for: copilot.description.message, copilot.family, - - const chars = Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) || 160; - const messages = [ - LanguageModelChatMessage.User( - `You are a CMS expert for Front Matter CMS and your task is to assist the user to generate a SEO friendly abstract/description for their article. When the user provides a title and/or content, you should use this information to generate the description. - - IMPORTANT: You are only allowed to respond with a text that should not exceed ${chars} characters in length.` - ) - ]; - - if (articleDetails && articleDetails.data?.title) { - messages.push(LanguageModelChatMessage.User( - `The title of the blog post is """${articleDetails.data.title}""".` - )); - } - - if (articleDetails && articleDetails.content) { - messages.push(LanguageModelChatMessage.User( - `The content of the blog post is: """${articleDetails.content}""".` - )); - } - - let chatResponse: LanguageModelChatResponse | undefined; - - try { - chatResponse = await model.sendRequest(messages, {}, new CancellationTokenSource().token); - } catch (err) { - Logger.error(`DataListener:copilotSuggestion:: ${(err as Error).message}`); + if (description) { panel.getWebview()?.postMessage({ command, requestId, - error: l10n.t(LocalizationKey.listenersPanelDataListenerAiSuggestTaxonomyNoDataError) + payload: description } as MessageHandlerData); - return; - } - - let allFragments = []; - for await (const fragment of chatResponse.text) { - allFragments.push(fragment); - } - - if (allFragments.length > 0) { - const description = allFragments.join(''); + } else { panel.getWebview()?.postMessage({ command, requestId, - payload: description.trim() + error: l10n.t(LocalizationKey.servicesCopilotGetChatResponseError) } as MessageHandlerData); } } + /** + * Suggests taxonomy using AI. + * @param command - The command string. + * @param requestId - The optional request ID. + */ private static async aiSuggestTaxonomy(command: string, requestId?: string) { if (!command || !requestId) { return; diff --git a/src/listeners/panel/TaxonomyListener.ts b/src/listeners/panel/TaxonomyListener.ts index e533c4e8..565e3992 100644 --- a/src/listeners/panel/TaxonomyListener.ts +++ b/src/listeners/panel/TaxonomyListener.ts @@ -15,6 +15,7 @@ import { PanelProvider } from '../../panelWebView/PanelProvider'; import { MessageHandlerData } from '@estruyf/vscode'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../localization'; +import { Copilot } from '../../services/Copilot'; export class TaxonomyListener extends BaseListener { /** @@ -66,9 +67,72 @@ export class TaxonomyListener extends BaseListener { case CommandToCode.aiSuggestTaxonomy: this.aiSuggestTaxonomy(msg.command, msg.requestId, msg.payload); break; + case CommandToCode.copilotSuggestTaxonomy: + this.copilotSuggestTaxonomy(msg.command, msg.requestId, msg.payload); + break; } } + + /** + * Suggests a taxonomy for a given command, request ID, and tag type. + * + * @param command - The command to execute. + * @param requestId - The ID of the request. + * @param type - The type of the tag. + * @returns A Promise that resolves to void. + */ + private static async copilotSuggestTaxonomy(command: string, requestId?: string, type?: TagType) { + if (!command || !requestId || !type) { + return; + } + + const article = ArticleHelper.getActiveFile(); + if (!article) { + return; + } + + const articleDetails = await ArticleHelper.getFrontMatterByPath(article); + if (!articleDetails) { + return; + } + + const extPath = Extension.getInstance().extensionPath; + const panel = PanelProvider.getInstance(extPath); + + const titleField = (Settings.get(SETTING_SEO_TITLE_FIELD) as string) || DefaultFields.Title; + const descriptionField = (Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string) || DefaultFields.Description; + + const tags = await Copilot.suggestTaxonomy( + articleDetails.data[titleField], + type, + articleDetails.data[descriptionField], + articleDetails.content + ); + + if (tags) { + panel.getWebview()?.postMessage({ + command, + requestId, + payload: tags + } as MessageHandlerData); + } else { + panel.getWebview()?.postMessage({ + command, + requestId, + error: l10n.t(LocalizationKey.servicesCopilotGetChatResponseError) + } as MessageHandlerData); + } + } + + /** + * Suggests taxonomy based on the provided command, request ID, and tag type. + * + * @param command - The command to execute. + * @param requestId - The ID of the request. + * @param type - The type of tag. + * @returns A Promise that resolves to void. + */ private static async aiSuggestTaxonomy(command: string, requestId?: string, type?: TagType) { if (!command || !requestId || !type) { return; diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index b4c15586..e12b8ec9 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -1680,6 +1680,10 @@ export enum LocalizationKey { * Use Front Matter AI to suggest {0} */ panelTagPickerAiSuggest = 'panel.tagPicker.ai.suggest', + /** + * Use GitHub Copilot to suggest {0} + */ + panelTagPickerCopilotSuggest = 'panel.tagPicker.copilot.suggest', /** * Generating suggestions... */ @@ -2316,6 +2320,10 @@ export enum LocalizationKey { * AI generated title */ helpersQuestionsContentTitleAiInputQuickPickAiSeparator = 'helpers.questions.contentTitle.aiInput.quickPick.ai.separator', + /** + * GitHub Copilot generated title + */ + helpersQuestionsContentTitleAiInputQuickPickCopilotSeparator = 'helpers.questions.contentTitle.aiInput.quickPick.copilot.separator', /** * Select a title */ @@ -2328,6 +2336,10 @@ export enum LocalizationKey { * Failed fetching the AI title. Please try to use your own title or try again later. */ helpersQuestionsContentTitleAiInputFailed = 'helpers.questions.contentTitle.aiInput.failed', + /** + * Failed fetching the GitHub Copilot title suggestions. Please try to use your own title or try again later. + */ + helpersQuestionsContentTitleCopilotInputFailed = 'helpers.questions.contentTitle.copilotInput.failed', /** * You did not specify a title for your content. */ @@ -2576,6 +2588,10 @@ export enum LocalizationKey { * No article data */ listenersPanelTaxonomyListenerAiSuggestTaxonomyNoDataError = 'listeners.panel.taxonomyListener.aiSuggestTaxonomy.noData.error', + /** + * Failed to get a response from the GitHub Copilot. + */ + servicesCopilotGetChatResponseError = 'services.copilot.getChatResponse.error', /** * Select the mode you want to use */ diff --git a/src/panelWebView/CommandToCode.ts b/src/panelWebView/CommandToCode.ts index 907e5620..c7e73ac5 100644 --- a/src/panelWebView/CommandToCode.ts +++ b/src/panelWebView/CommandToCode.ts @@ -42,7 +42,8 @@ export enum CommandToCode { stopServer = 'stop-server', aiSuggestTaxonomy = 'ai-suggest-taxonomy', aiSuggestDescription = 'ai-suggest-description', - copilotDescription = 'copilot-suggest-description', + copilotSuggestDescription = 'copilot-suggest-description', + copilotSuggestTaxonomy = 'copilot-suggest-taxonomy', searchByType = 'search-by-type', processMediaData = 'process-media-data', isServerStarted = 'is-server-started' diff --git a/src/panelWebView/components/Fields/FieldTitle.tsx b/src/panelWebView/components/Fields/FieldTitle.tsx index 3f83cb9c..a2a71cb9 100644 --- a/src/panelWebView/components/Fields/FieldTitle.tsx +++ b/src/panelWebView/components/Fields/FieldTitle.tsx @@ -22,8 +22,8 @@ export const FieldTitle: React.FunctionComponent = ({ }, [icon]); return ( -
-