diff --git a/CHANGELOG.md b/CHANGELOG.md index 743a626e..624fa569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - [#834](https://github.com/estruyf/vscode-front-matter/issues/834): Added the ability to create new data files for a data folder - [#841](https://github.com/estruyf/vscode-front-matter/issues/841): Enable placeholders for file prefixes - [#846](https://github.com/estruyf/vscode-front-matter/issues/846): Added GitHub Copilot action for title field +- [#848](https://github.com/estruyf/vscode-front-matter/issues/848): Set the default GitHub Copilot model to `gpt-4o-mini` ### 🐞 Fixes diff --git a/package.json b/package.json index 45482216..74f9c3c9 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "color": "#0e131f", "theme": "dark" }, - "badges": [ - { + "badges": [{ "description": "version", "url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square", "href": "https://github.com/estruyf/vscode-front-matter" @@ -71,8 +70,7 @@ "**/.frontmatter/config/*.json": "jsonc" } }, - "keybindings": [ - { + "keybindings": [{ "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -96,23 +94,19 @@ } ], "viewsContainers": { - "activitybar": [ - { - "id": "frontmatter-explorer", - "title": "FM", - "icon": "$(fm-logo)" - } - ] + "activitybar": [{ + "id": "frontmatter-explorer", + "title": "FM", + "icon": "$(fm-logo)" + }] }, "views": { - "frontmatter-explorer": [ - { - "id": "frontMatter.explorer", - "name": "Front Matter", - "icon": "$(fm-logo)", - "type": "webview" - } - ] + "frontmatter-explorer": [{ + "id": "frontMatter.explorer", + "name": "Front Matter", + "icon": "$(fm-logo)", + "type": "webview" + }] }, "configuration": { "title": "%settings.configuration.title%", @@ -180,8 +174,7 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [ - { + "oneOf": [{ "enum": [ "md", "mdx" @@ -197,8 +190,7 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [ - { + "oneOf": [{ "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -550,8 +542,7 @@ "categories" ], "markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%", - "items": [ - { + "items": [{ "type": "string", "enum": [ "contentFolders", @@ -625,8 +616,7 @@ "command": { "$id": "#scriptCommand", "type": "string", - "anyOf": [ - { + "anyOf": [{ "enum": [ "node", "bash", @@ -822,8 +812,7 @@ "title", "file" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -891,8 +880,7 @@ "id", "path" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -1133,29 +1121,26 @@ } } }, - "default": [ - { - "name": "default", - "fileTypes": null, - "fields": [ - { - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Caption", - "name": "caption", - "type": "string" - }, - { - "title": "Alt text", - "name": "alt", - "type": "string" - } - ] - } - ], + "default": [{ + "name": "default", + "fileTypes": null, + "fields": [{ + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Caption", + "name": "caption", + "type": "string" + }, + { + "title": "Alt text", + "name": "alt", + "type": "string" + } + ] + }], "scope": "Media" }, "frontMatter.media.supportedMimeTypes": { @@ -1258,8 +1243,7 @@ "fileType": { "type": "string", "default": "", - "oneOf": [ - { + "oneOf": [{ "enum": [ "md", "mdx" @@ -1398,8 +1382,7 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1600,8 +1583,7 @@ "type", "name" ], - "allOf": [ - { + "allOf": [{ "if": { "properties": { "type": { @@ -1813,51 +1795,48 @@ "fields" ] }, - "default": [ - { - "name": "default", - "pageBundle": false, - "fields": [ - { - "title": "Title", - "name": "title", - "type": "string" - }, - { - "title": "Description", - "name": "description", - "type": "string" - }, - { - "title": "Publishing date", - "name": "date", - "type": "datetime", - "default": "{{now}}", - "isPublishDate": true - }, - { - "title": "Content preview", - "name": "preview", - "type": "image" - }, - { - "title": "Is in draft", - "name": "draft", - "type": "boolean" - }, - { - "title": "Tags", - "name": "tags", - "type": "tags" - }, - { - "title": "Categories", - "name": "categories", - "type": "categories" - } - ] - } - ], + "default": [{ + "name": "default", + "pageBundle": false, + "fields": [{ + "title": "Title", + "name": "title", + "type": "string" + }, + { + "title": "Description", + "name": "description", + "type": "string" + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Content preview", + "name": "preview", + "type": "image" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "boolean" + }, + { + "title": "Tags", + "name": "tags", + "type": "tags" + }, + { + "title": "Categories", + "name": "categories", + "type": "categories" + } + ] + }], "scope": "Taxonomy" }, "frontMatter.taxonomy.customTaxonomy": { @@ -1870,8 +1849,7 @@ "type": "string", "description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -2065,13 +2043,12 @@ }, "frontMatter.copilot.family": { "type": "string", - "default": "gpt-3.5-turbo", + "default": "gpt-4o-mini", "markdownDescription": "%setting.frontMatter.copilot.family.markdownDescription%" } } }, - "commands": [ - { + "commands": [{ "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2403,21 +2380,16 @@ } } ], - "submenus": [ - { - "id": "frontmatter.submenu", - "label": "Front Matter" - } - ], + "submenus": [{ + "id": "frontmatter.submenu", + "label": "Front Matter" + }], "menus": { - "webview/context": [ - { - "command": "workbench.action.webview.openDeveloperTools", - "when": "frontMatter:isDevelopment" - } - ], - "editor/title": [ - { + "webview/context": [{ + "command": "workbench.action.webview.openDeveloperTools", + "when": "frontMatter:isDevelopment" + }], + "editor/title": [{ "command": "frontMatter.markup.heading", "group": "navigation@-133", "when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg" @@ -2503,14 +2475,11 @@ "when": "resourceFilename == 'frontmatter.json'" } ], - "explorer/context": [ - { - "submenu": "frontmatter.submenu", - "group": "frontmatter@1" - } - ], - "frontmatter.submenu": [ - { + "explorer/context": [{ + "submenu": "frontmatter.submenu", + "group": "frontmatter@1" + }], + "frontmatter.submenu": [{ "command": "frontMatter.createFromTemplate", "when": "explorerResourceIsFolder", "group": "frontmatter@1" @@ -2526,8 +2495,7 @@ "group": "frontmatter@3" } ], - "commandPalette": [ - { + "commandPalette": [{ "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2704,8 +2672,7 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [ - { + "view/title": [{ "command": "frontMatter.docs", "group": "navigation@-1", "when": "view == frontMatter.explorer" @@ -2742,16 +2709,13 @@ } ] }, - "languages": [ - { - "id": "frontmatter.project.output", - "mimetypes": [ - "text/x-code-output" - ] - } - ], - "grammars": [ - { + "languages": [{ + "id": "frontmatter.project.output", + "mimetypes": [ + "text/x-code-output" + ] + }], + "grammars": [{ "path": "./syntaxes/hugo.tmLanguage.json", "scopeName": "frontmatter.markdown.hugo", "injectTo": [ @@ -2764,48 +2728,45 @@ "path": "./syntaxes/frontmatter-output.tmLanguage.json" } ], - "walkthroughs": [ - { - "id": "frontmatter.welcome", - "title": "Get started with Front Matter", - "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", - "steps": [ - { - "id": "frontmatter.welcome.init", - "title": "Get started", - "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", - "media": { - "markdown": "assets/walkthrough/get-started.md" - }, - "completionEvents": [ - "onContext:frontMatterInitialized" - ] + "walkthroughs": [{ + "id": "frontmatter.welcome", + "title": "Get started with Front Matter", + "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", + "steps": [{ + "id": "frontmatter.welcome.init", + "title": "Get started", + "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", + "media": { + "markdown": "assets/walkthrough/get-started.md" }, - { - "id": "frontmatter.welcome.documentation", - "title": "Documentation", - "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", - "media": { - "markdown": "assets/walkthrough/documentation.md" - }, - "completionEvents": [ - "onLink:https://frontmatter.codes/docs" - ] + "completionEvents": [ + "onContext:frontMatterInitialized" + ] + }, + { + "id": "frontmatter.welcome.documentation", + "title": "Documentation", + "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", + "media": { + "markdown": "assets/walkthrough/documentation.md" }, - { - "id": "frontmatter.welcome.supporter", - "title": "Support the project", - "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", - "media": { - "markdown": "assets/walkthrough/support-the-project.md" - }, - "completionEvents": [ - "onLink:https://github.com/sponsors/estruyf" - ] - } - ] - } - ] + "completionEvents": [ + "onLink:https://frontmatter.codes/docs" + ] + }, + { + "id": "frontmatter.welcome.supporter", + "title": "Support the project", + "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", + "media": { + "markdown": "assets/walkthrough/support-the-project.md" + }, + "completionEvents": [ + "onLink:https://github.com/sponsors/estruyf" + ] + } + ] + }] }, "scripts": { "dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*", @@ -2932,4 +2893,4 @@ "vsce": { "dependencies": false } -} +} \ No newline at end of file diff --git a/src/services/Copilot.ts b/src/services/Copilot.ts index 8fc233ef..cb31523a 100644 --- a/src/services/Copilot.ts +++ b/src/services/Copilot.ts @@ -17,7 +17,7 @@ import { TaxonomyType } from '../models'; export class Copilot { private static personality = - 'You are a CMS expert for Front Matter CMS and your task is to assist the user to help generate content for their article.'; + 'You are a CMS expert specializing in Front Matter CMS. Your task is to assist the user in generating optimized content for their article.'; /** * Checks if the GitHub Copilot extension is installed. @@ -39,32 +39,35 @@ export class Copilot { return; } - const chars = Settings.get(SETTING_SEO_TITLE_LENGTH) || 60; - const messages = [ - LanguageModelChatMessage.User(Copilot.personality), - LanguageModelChatMessage.User( - `The user wants you to create a SEO friendly title. You should give the user a couple of suggestions based on the provided title. - - IMPORTANT: You are only allowed to respond with a text that should not exceed ${chars} characters in length. - - Desired format: just a string and wrapped in double quotes, e.g. "My first blog post". Each suggestion is separated by a new line.` - ), - LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) - ]; - - const chatResponse = await this.getChatResponse(messages); - if (!chatResponse) { - return; + try { + const chars = Settings.get(SETTING_SEO_TITLE_LENGTH) || 60; + const messages = [ + LanguageModelChatMessage.User(Copilot.personality), + LanguageModelChatMessage.User( + `Generate an SEO-friendly title based on the provided one. Offer a few suggestions, ensuring each does not exceed ${chars} characters. + + Each title suggestion should have the following response format: a single string wrapped in double quotes, e.g., "My first blog post" with each suggestion on a new line.` + ), + LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) + ]; + + const chatResponse = await this.getChatResponse(messages); + if (!chatResponse) { + return; + } + + let titles = chatResponse.split('\n').map((title) => title.trim()); + // Remove 1. or - from the beginning of the title + titles = titles.map((title) => title.replace(/^\d+\.\s+|-/, '').trim()); + // Only take the titles wrapped in quotes + titles = titles.filter((title) => title.startsWith('"') && title.endsWith('"')); + // Remove the quotes from the beginning and end of the title + titles = titles.map((title) => title.slice(1, -1)); + return titles; + } catch (err) { + Logger.error(`Copilot:suggestTitles:: ${(err as Error).message}`); + return []; } - - let titles = chatResponse.split('\n').map((title) => title.trim()); - // Remove 1. or - from the beginning of the title - titles = titles.map((title) => title.replace(/^\d+\.\s+|-/, '').trim()); - // Only take the titles wrapped in quotes - titles = titles.filter((title) => title.startsWith('"') && title.endsWith('"')); - // Remove the quotes from the beginning and end of the title - titles = titles.map((title) => title.slice(1, -1)); - return titles; } /** @@ -79,25 +82,38 @@ export class Copilot { return; } - const chars = Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) || 160; - const messages = [ - LanguageModelChatMessage.User(Copilot.personality), - LanguageModelChatMessage.User( - `The user wants you to create a SEO friendly abstract/description. 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.` - ), - LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) - ]; - - if (content) { - messages.push( - LanguageModelChatMessage.User(`The content of the blog post is: """${content}""".`) - ); - } + try { + const chars = Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) || 160; + const messages = [ + LanguageModelChatMessage.User(Copilot.personality), + LanguageModelChatMessage.User( + `Generate an SEO-friendly description using the provided title and/or content. Ensure the description does not exceed ${chars} characters. + +Response format: a single string wrapped in double quotes, e.g., "Boost your website's performance with these easy-to-follow speed optimization tips.".` + ), + LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) + ]; + + if (content) { + messages.push( + LanguageModelChatMessage.User(`The content of the blog post is: """${content}""".`) + ); + } + + const chatResponse = await this.getChatResponse(messages); - const chatResponse = await this.getChatResponse(messages); - return chatResponse; + if (!chatResponse) { + return; + } + + let description = chatResponse.trim(); + description = description.startsWith('"') ? description.slice(1) : description; + description = description.endsWith('"') ? description.slice(0, -1) : description; + return description; + } catch (err) { + Logger.error(`Copilot:suggestDescription:: ${(err as Error).message}`); + return ''; + } } /** @@ -119,58 +135,70 @@ export class Copilot { return; } - const messages = [ - LanguageModelChatMessage.User(Copilot.personality), - LanguageModelChatMessage.User( - `The user wants you to suggest some taxonomy tags. When the user provides a title, description, list of available taxonomy tags, and/or content, you should use this information to generate the tags. - - IMPORTANT: You are only allowed to respond with a list of tags separated by commas. Example: tag1, tag2, tag3.` - ), - LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) - ]; - - if (description) { - messages.push( - LanguageModelChatMessage.User(`The description of the blog post is: """${description}""".`) - ); - } - - let options = - tagType === TagType.tags - ? await TaxonomyHelper.get(TaxonomyType.Tag) - : await TaxonomyHelper.get(TaxonomyType.Category); - const optionsString = options?.join(',') || ''; + try { + let type = tagType === TagType.tags ? 'tags' : 'categories'; + if (tagType === TagType.custom) { + type = 'tags'; + } - if (optionsString) { - messages.push( + const messages = [ + LanguageModelChatMessage.User(Copilot.personality), LanguageModelChatMessage.User( - `The available taxonomy tags are: ${optionsString}. Please select the tags that are relevant to the article. You are allowed to suggest a maximum of 5 tags and suggest new tags if necessary.` - ) - ); - } - - if (content) { - messages.push( - LanguageModelChatMessage.User(`The content of the blog post is: """${content}""".`) - ); - } - - const chatResponse = await this.getChatResponse(messages); - - if (!chatResponse) { - return; - } - - // If the chat response contains a colon character, we take the text after the colon as the response. - if (chatResponse.includes(':')) { - return chatResponse - .split(':')[1] - .split(',') - .map((tag) => tag.trim()); + `Generate relevant taxonomy ${type} based on the provided title, description, available ${type}, and/or content. Respond with a list of ${type} separated by commas. + +Example: SEO, website optimization, digital marketing.` + ), + LanguageModelChatMessage.User(`The title of the blog post is """${title}""".`) + ]; + + if (description) { + messages.push( + LanguageModelChatMessage.User( + `The description of the blog post is: """${description}""".` + ) + ); + } + + let options = + tagType === TagType.tags + ? await TaxonomyHelper.get(TaxonomyType.Tag) + : await TaxonomyHelper.get(TaxonomyType.Category); + const optionsString = options?.join(',') || ''; + + if (optionsString) { + messages.push( + LanguageModelChatMessage.User( + `Based on the provided title, description, and/or content, select relevant ${tagType} from the available taxonomy list: ${optionsString}. You may suggest up to 5 tags and include new ones if necessary.` + ) + ); + } + + if (content) { + messages.push( + LanguageModelChatMessage.User(`The content of the blog post is: """${content}""".`) + ); + } + + const chatResponse = await this.getChatResponse(messages); + + if (!chatResponse) { + return; + } + + // If the chat response contains a colon character, we take the text after the colon as the response. + if (chatResponse.includes(':')) { + return chatResponse + .split(':')[1] + .split(',') + .map((tag) => tag.trim()); + } + + // Otherwise, we split the response by commas. + return chatResponse.split(',').map((tag) => tag.trim()); + } catch (err) { + Logger.error(`Copilot:suggestTaxonomy:: ${(err as Error).message}`); + return []; } - - // Otherwise, we split the response by commas. - return chatResponse.split(',').map((tag) => tag.trim()); } /** @@ -202,9 +230,11 @@ export class Copilot { * @returns A Promise that resolves to the chat model. */ private static async getModel() { + // const models = await lm.selectChatModels(); + // console.log(models); const [model] = await lm.selectChatModels({ vendor: 'copilot', - family: Settings.get(SETTING_COPILOT_FAMILY) || 'gpt-3.5-turbo' + family: Settings.get(SETTING_COPILOT_FAMILY) || 'gpt-4o-mini' }); return model;