diff --git a/CHANGELOG.md b/CHANGELOG.md index 50138075..f5bfd614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,13 @@ - External UI script support for dashboards - Visual Studio Code Theming support for the dashboards +- Front matter AI 🤖 > **Info**: To enable the experimental features you need to set the `frontMatter.experimental` setting to `true`. ### ✨ New features +- [#530](https://github.com/estruyf/vscode-front-matter/issues/530): Implementation of the Front Matter AI 🤖 powered by [mendable.ai](https://mendable.ai) - [#513](https://github.com/estruyf/vscode-front-matter/issues/513): Added support for external UI scripts to add custom HTML on the dashboard elements ### 🎨 Enhancements diff --git a/assets/icons/chatbot-dark.svg b/assets/icons/chatbot-dark.svg new file mode 100644 index 00000000..718e178a --- /dev/null +++ b/assets/icons/chatbot-dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/chatbot-light.svg b/assets/icons/chatbot-light.svg new file mode 100644 index 00000000..5489e3e7 --- /dev/null +++ b/assets/icons/chatbot-light.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/package.json b/package.json index 54a288aa..24c71a71 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" @@ -55,8 +54,7 @@ ], "main": "./dist/extension.js", "contributes": { - "keybindings": [ - { + "keybindings": [{ "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -74,24 +72,20 @@ } ], "viewsContainers": { - "activitybar": [ - { - "id": "frontmatter-explorer", - "title": "Front Matter", - "icon": "assets/frontmatter-short-min.svg" - } - ] + "activitybar": [{ + "id": "frontmatter-explorer", + "title": "Front Matter", + "icon": "assets/frontmatter-short-min.svg" + }] }, "views": { - "frontmatter-explorer": [ - { - "id": "frontMatter.explorer", - "name": "Front Matter", - "icon": "assets/frontmatter-short-min.svg", - "contextualTitle": "Front Matter", - "type": "webview" - } - ] + "frontmatter-explorer": [{ + "id": "frontMatter.explorer", + "name": "Front Matter", + "icon": "assets/frontmatter-short-min.svg", + "contextualTitle": "Front Matter", + "type": "webview" + }] }, "configuration": { "title": "Front Matter: use frontmatter.json for shared team settings", @@ -126,8 +120,7 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [ - { + "oneOf": [{ "enum": [ "md", "mdx" @@ -143,8 +136,7 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [ - { + "oneOf": [{ "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -471,8 +463,7 @@ "command": { "$id": "#scriptCommand", "type": "string", - "oneOf": [ - { + "oneOf": [{ "enum": [ "node", "bash", @@ -646,8 +637,7 @@ "title", "file" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -701,8 +691,7 @@ "id", "path" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -1048,8 +1037,7 @@ "default": "", "description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1209,8 +1197,7 @@ "type", "name" ], - "allOf": [ - { + "allOf": [{ "if": { "properties": { "type": { @@ -1368,51 +1355,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": { @@ -1425,8 +1409,7 @@ "type": "string", "description": "ID for your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1608,8 +1591,7 @@ } } }, - "commands": [ - { + "commands": [{ "command": "frontMatter.config.reload", "title": "Reload config", "category": "Front Matter" @@ -1870,8 +1852,12 @@ }, { "command": "frontMatter.chatbot", - "title": "Ask a bot to help you configure Front Matter", - "category": "Front Matter" + "title": "Ask the Front Matter AI for help", + "category": "Front Matter", + "icon": { + "light": "assets/icons/chatbot-light.svg", + "dark": "assets/icons/chatbot-dark.svg" + } }, { "command": "frontMatter.promoteSettings", @@ -1928,15 +1914,12 @@ "category": "Front Matter" } ], - "submenus": [ - { - "id": "frontmatter.submenu", - "label": "Front Matter" - } - ], + "submenus": [{ + "id": "frontmatter.submenu", + "label": "Front Matter" + }], "menus": { - "editor/title": [ - { + "editor/title": [{ "command": "frontMatter.markup.heading", "group": "navigation@-133", "when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg" @@ -2010,16 +1993,18 @@ "command": "frontMatter.dashboard.close", "group": "navigation@-98", "when": "frontMatter:enabled == true && frontMatter:dashboard:open == true" + }, + { + "command": "frontMatter.chatbot", + "group": "navigation@-97", + "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" @@ -2035,8 +2020,7 @@ "group": "frontmatter@3" } ], - "commandPalette": [ - { + "commandPalette": [{ "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2177,75 +2161,74 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [ - { - "command": "frontMatter.collapseSections", + "view/title": [{ + "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" }, { - "command": "frontMatter.mode.switch", + "command": "frontMatter.collapseSections", "group": "navigation@1", + "when": "view == frontMatter.explorer" + }, + { + "command": "frontMatter.mode.switch", + "group": "navigation@2", "when": "view == frontMatter.explorer && frontMatter:has:modes == true" }, { "command": "frontMatter.dashboard", - "group": "navigation@2", + "group": "navigation@3", "when": "view == frontMatter.explorer || view == explorer" } ] }, - "grammars": [ - { - "path": "./syntaxes/hugo.tmLanguage.json", - "scopeName": "frontmatter.markdown.hugo", - "injectTo": [ - "text.html.markdown" - ] - } - ], - "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" - ] + "grammars": [{ + "path": "./syntaxes/hugo.tmLanguage.json", + "scopeName": "frontmatter.markdown.hugo", + "injectTo": [ + "text.html.markdown" + ] + }], + "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-all --parallel watch:*", @@ -2375,4 +2358,4 @@ "vsce": { "dependencies": false } -} +} \ No newline at end of file diff --git a/src/commands/Chatbot.ts b/src/commands/Chatbot.ts index 2d35b9e5..56188c96 100644 --- a/src/commands/Chatbot.ts +++ b/src/commands/Chatbot.ts @@ -1,26 +1,9 @@ -import { processFmPlaceholders } from './../helpers/processFmPlaceholders'; -import { processPathPlaceholders } from './../helpers/processPathPlaceholders'; import { Telemetry } from './../helpers/Telemetry'; -import { - SETTING_PREVIEW_HOST, - SETTING_PREVIEW_PATHNAME, - CONTEXT, - TelemetryEvent, - PreviewCommands, - SETTING_EXPERIMENTAL, - SETTING_DATE_FORMAT -} from './../constants'; -import { ArticleHelper } from './../helpers/ArticleHelper'; +import { TelemetryEvent, PreviewCommands, SETTING_EXPERIMENTAL } from './../constants'; import { join } from 'path'; -import { commands, env, Uri, ViewColumn, window } from 'vscode'; -import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers'; -import { ContentFolder, ContentType, PreviewSettings } from '../models'; -import { format } from 'date-fns'; -import { DateHelper } from '../helpers/DateHelper'; -import { Article } from '.'; -import { urlJoin } from 'url-join-ts'; +import { commands, Uri, ViewColumn, window } from 'vscode'; +import { Extension, Settings } from '../helpers'; import { WebviewHelper } from '@estruyf/vscode'; -import { Folders } from './Folders'; export class Chatbot { /** @@ -30,7 +13,7 @@ export class Chatbot { // Create the preview webview const webView = window.createWebviewPanel( 'frontMatterChatbot', - 'Front Matter - Ask me anything', + 'Front Matter AI - Ask me anything', { viewColumn: ViewColumn.Beside, preserveFocus: true diff --git a/src/components/icons/ChatIcon.tsx b/src/components/icons/ChatIcon.tsx new file mode 100644 index 00000000..0712167e --- /dev/null +++ b/src/components/icons/ChatIcon.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export interface IChatIconProps { + className?: string; +} + +export const ChatIcon: React.FunctionComponent = ({ className }: React.PropsWithChildren) => { + return ( + + + + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Chatbot/Answer.tsx b/src/dashboardWebView/components/Chatbot/Answer.tsx new file mode 100644 index 00000000..19570109 --- /dev/null +++ b/src/dashboardWebView/components/Chatbot/Answer.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { Feedback } from './Feedback'; + +export interface IAnswerProps { + answer: string; + answerId: number; + sources: string[]; +} + +export const Answer: React.FunctionComponent = ({ + answer, + answerId, + sources, +}: React.PropsWithChildren) => { + if (!answer) { + return null; + } + + return ( +
  • +
    +

    Answer

    + + +
    + + + + { + sources && sources.length > 0 && ( +
    +

    Resources

    +
      + {sources.map((source, idx) => ( +
    • + {source} +
    • + ))} +
    +
    + ) + } + +
    + Warning: Anwers might be wrong. In case of doubt, please consult the docs. +
    +
  • + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Chatbot/Chatbot.tsx b/src/dashboardWebView/components/Chatbot/Chatbot.tsx index 46d4b1b4..d2e38e08 100644 --- a/src/dashboardWebView/components/Chatbot/Chatbot.tsx +++ b/src/dashboardWebView/components/Chatbot/Chatbot.tsx @@ -2,36 +2,35 @@ import * as React from 'react'; import { InitResponse } from './models/InitResponse'; import { NewConversationResponse } from './models/NewConversationResponse'; import { fetchEventSource } from '@microsoft/fetch-event-source'; -import { ChatIcon, PaperAirplaneIcon, ThumbDownIcon, ThumbUpIcon } from '@heroicons/react/outline'; -import { ThumbDownIcon as ThumbDownSolidIcon, ThumbUpIcon as ThumbUpSolidIcon } from '@heroicons/react/solid'; -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' import { useCallback, useEffect } from 'react'; +import { Header } from './Header'; +import { Chatbox } from './Chatbox'; +import { Loader } from './Loader'; +import { QuestionAnswer } from './QuestionAnswer'; +import { Placeholder } from './Placeholder'; +import { useSettingsContext } from '../../providers/SettingsProvider'; export interface IChatbotProps { } -export const Chatbot: React.FunctionComponent = (props: React.PropsWithChildren) => { +export const Chatbot: React.FunctionComponent = ({ }: React.PropsWithChildren) => { + const { aiKey, aiUrl } = useSettingsContext(); const [company, setCompany] = React.useState(undefined); const [chatId, setChatId] = React.useState(undefined); - const [message, setMessage] = React.useState(""); const [questions, setQuestions] = React.useState([]); const [answers, setAnswers] = React.useState([]); const [answerIds, setAnswerIds] = React.useState([]); const [sources, setSources] = React.useState([]); const [loading, setLoading] = React.useState(false); - const [upVote, setUpVotes] = React.useState([]); - const [downVote, setDownvotes] = React.useState([]); - const init = async () => { setLoading(true); - const initResponse = await fetch('https://aijsplayground-production.up.railway.app/initializeMendable', { + const initResponse = await fetch(`${aiUrl}/initializeMendable`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - anon_key: "466f5321-12d9-4d64-9e5b-ea5db41ed2ba" + anon_key: aiKey }) }); @@ -41,13 +40,13 @@ export const Chatbot: React.FunctionComponent = (props: React.Pro const initJson: InitResponse = await initResponse.json(); - const newChatResponse = await fetch('https://aijsplayground-production.up.railway.app/newConversation', { + const newChatResponse = await fetch(`${aiUrl}/newConversation`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - anon_key: "466f5321-12d9-4d64-9e5b-ea5db41ed2ba", + anon_key: aiKey, messages: [] }) }) @@ -63,31 +62,7 @@ export const Chatbot: React.FunctionComponent = (props: React.Pro setLoading(false); }; - const onUpVote = useCallback(async (index: number) => { - setUpVotes(prev => [...prev, index]) - setDownvotes(prev => prev.filter(i => i !== index)) - callVote(index, true) - }, []); - - const onDownVote = useCallback(async (index: number) => { - setDownvotes(prev => [...prev, index]) - setUpVotes(prev => prev.filter(i => i !== index)) - callVote(index, false) - }, []); - - const callVote = async (index: number, vote: boolean) => { - await fetch(`https://aijsplayground-production.up.railway.app/updateMessageRating/${index}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ratingValue: vote ? 1 : -1, - }) - }) - } - - const callChatbot = useCallback(async () => { + const callChatbot = useCallback(async (message) => { const nrOfQuestions = questions.length + 1; setLoading(true); @@ -96,15 +71,11 @@ export const Chatbot: React.FunctionComponent = (props: React.Pro setSources(prev => [...prev, []]) setAnswerIds(prev => [...prev, 0]) - setTimeout(() => { - setMessage("") - }, 0); - if (!company || !chatId) { return; } - await fetchEventSource('https://aijsplayground-production.up.railway.app/qaChat', { + await fetchEventSource(`${aiUrl}/qaChat`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -159,7 +130,7 @@ export const Chatbot: React.FunctionComponent = (props: React.Pro } } }); - }, [company, chatId, message, questions]); + }, [company, chatId, questions]); useEffect(() => { init(); @@ -167,130 +138,32 @@ export const Chatbot: React.FunctionComponent = (props: React.Pro return (
    -
    -

    - - Ask Font Matter AI -

    -

    - Our AI, powered by mendable.ai, has processed the documentation and can assist you with any queries regarding Front Matter. Go ahead and ask away! -

    -
    +
    -
    +
    -
    - { - questions.map((question, idx) => ( -
      -
    • {question}
    • - {answers.length > 0 && answers[idx] && ( -
    • -
      -

      Answer

      - - { - answerIds[idx] && ( -
      - - -
      - ) - } -
      - - - - { - sources[idx].length > 0 && sources[idx] && ( -
      -

      Resources

      -
        - {sources[idx].map((source, idx) => ( -
      • - {source} -
      • - ))} -
      -
      - ) - } - -
      - Warning: Anwers might be wrong. In case of doubt, please consult the docs. -
      -
    • - )} -
    - )) - } -
    + { + questions && questions.length > 0 ? questions.map((question, idx) => ( + + )) : ( + + ) + } { - loading && ( -
    -
    -
    -
    -
    -
    -
    - ) + loading && (questions && questions.length > 0) && }
    -