diff --git a/CHANGELOG.md b/CHANGELOG.md index fff257f8..8a98bd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ ### ✨ 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 +- [#530](https://github.com/estruyf/vscode-front-matter/issues/530): Implementation of the Front Matter AI 🤖 powered by [mendable.ai](https://mendable.ai) ### 🎨 Enhancements diff --git a/package-lock.json b/package-lock.json index 9f51a6e6..b8f6ec3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@headlessui/react": "1.5.0", "@heroicons/react": "1.0.4", "@iarna/toml": "2.2.3", - "@microsoft/fetch-event-source": "^2.0.1", "@octokit/rest": "^18.12.0", "@popperjs/core": "^2.11.6", "@sentry/react": "^6.13.3", @@ -501,12 +500,6 @@ "exenv-es6": "^1.0.0" } }, - "node_modules/@microsoft/fetch-event-source": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", - "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -13178,12 +13171,6 @@ "exenv-es6": "^1.0.0" } }, - "@microsoft/fetch-event-source": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", - "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", - "dev": true - }, "@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, diff --git a/package.json b/package.json index 24c71a71..3b9f9fe3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "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" @@ -54,7 +55,8 @@ ], "main": "./dist/extension.js", "contributes": { - "keybindings": [{ + "keybindings": [ + { "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -72,20 +74,24 @@ } ], "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", @@ -120,7 +126,8 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [{ + "oneOf": [ + { "enum": [ "md", "mdx" @@ -136,7 +143,8 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [{ + "oneOf": [ + { "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -463,7 +471,8 @@ "command": { "$id": "#scriptCommand", "type": "string", - "oneOf": [{ + "oneOf": [ + { "enum": [ "node", "bash", @@ -637,7 +646,8 @@ "title", "file" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -691,7 +701,8 @@ "id", "path" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -1037,7 +1048,8 @@ "default": "", "description": "The ID of your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -1197,7 +1209,8 @@ "type", "name" ], - "allOf": [{ + "allOf": [ + { "if": { "properties": { "type": { @@ -1355,48 +1368,51 @@ "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": { @@ -1409,7 +1425,8 @@ "type": "string", "description": "ID for your taxonomy field. It cannot contain the \"tags\" or \"categories\" value.", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -1591,7 +1608,8 @@ } } }, - "commands": [{ + "commands": [ + { "command": "frontMatter.config.reload", "title": "Reload config", "category": "Front Matter" @@ -1914,12 +1932,15 @@ "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" @@ -2000,11 +2021,14 @@ "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" @@ -2020,7 +2044,8 @@ "group": "frontmatter@3" } ], - "commandPalette": [{ + "commandPalette": [ + { "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2161,7 +2186,8 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [{ + "view/title": [ + { "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2183,52 +2209,57 @@ } ] }, - "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" + "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" + ] }, - "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.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": [ - "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" - ] - } - ] - }] + { + "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:*", @@ -2258,7 +2289,6 @@ "@headlessui/react": "1.5.0", "@heroicons/react": "1.0.4", "@iarna/toml": "2.2.3", - "@microsoft/fetch-event-source": "^2.0.1", "@octokit/rest": "^18.12.0", "@popperjs/core": "^2.11.6", "@sentry/react": "^6.13.3", @@ -2358,4 +2388,4 @@ "vsce": { "dependencies": false } -} \ No newline at end of file +} diff --git a/src/dashboardWebView/components/Chatbot/Chatbot.tsx b/src/dashboardWebView/components/Chatbot/Chatbot.tsx index d2e38e08..f5ec69b6 100644 --- a/src/dashboardWebView/components/Chatbot/Chatbot.tsx +++ b/src/dashboardWebView/components/Chatbot/Chatbot.tsx @@ -1,7 +1,4 @@ import * as React from 'react'; -import { InitResponse } from './models/InitResponse'; -import { NewConversationResponse } from './models/NewConversationResponse'; -import { fetchEventSource } from '@microsoft/fetch-event-source'; import { useCallback, useEffect } from 'react'; import { Header } from './Header'; import { Chatbox } from './Chatbox'; @@ -9,11 +6,12 @@ import { Loader } from './Loader'; import { QuestionAnswer } from './QuestionAnswer'; import { Placeholder } from './Placeholder'; import { useSettingsContext } from '../../providers/SettingsProvider'; +import { AiInitResponse } from './models/AiInitResponse'; export interface IChatbotProps { } export const Chatbot: React.FunctionComponent = ({ }: React.PropsWithChildren) => { - const { aiKey, aiUrl } = useSettingsContext(); + const { aiUrl } = useSettingsContext(); const [company, setCompany] = React.useState(undefined); const [chatId, setChatId] = React.useState(undefined); const [questions, setQuestions] = React.useState([]); @@ -24,46 +22,29 @@ export const Chatbot: React.FunctionComponent = ({ }: React.Props const init = async () => { setLoading(true); - const initResponse = await fetch(`${aiUrl}/initializeMendable`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - anon_key: aiKey - }) - }); + const initResponse = await fetch(`${aiUrl}/api/ai-init`); if (!initResponse.ok) { return; } - const initJson: InitResponse = await initResponse.json(); + const initJson: AiInitResponse = await initResponse.json(); - const newChatResponse = await fetch(`${aiUrl}/newConversation`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - anon_key: aiKey, - messages: [] - }) - }) - - if (!newChatResponse.ok) { + if (!initJson.company || !initJson.chatId) { return; } - const newChat: NewConversationResponse = await newChatResponse.json(); - - setCompany(initJson.company.name); - setChatId(newChat.conversation_id); + setCompany(initJson.company); + setChatId(initJson.chatId); setLoading(false); }; - const callChatbot = useCallback(async (message) => { - const nrOfQuestions = questions.length + 1; + const onTrigger = useCallback(async (message: string) => { + callChatbot(message, questions.length); + }, [questions, company, chatId]); + + const callChatbot = useCallback(async (message, questionLenght) => { + const nrOfQuestions = questionLenght + 1; setLoading(true); setQuestions(prev => [...prev, message]) @@ -75,62 +56,69 @@ export const Chatbot: React.FunctionComponent = ({ }: React.Props return; } - await fetchEventSource(`${aiUrl}/qaChat`, { + const response = await fetch(`${aiUrl}/api/ai-chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'accept': 'application/json', + 'accept': '*', }, body: JSON.stringify({ - company: company, - conversation_id: chatId, - history: [{ prompt: "", response: "", sources: [] }], + company, + chatId, question: message, }), - onmessage: (event) => { - setLoading(false); - const data = JSON.parse(event.data); - const chunk = data.chunk; - - if (chunk === "<|source|>") { - setSources(prev => { - const metadata = [...new Set(data.metadata.map((m: any) => m.link))] as string[] - - const crntSources: string[][] = Object.assign([], prev) - if (crntSources.length === nrOfQuestions) { - crntSources[nrOfQuestions - 1] = metadata; - } else { - crntSources.push(metadata); - } - - return crntSources; - }); - } else if (chunk === "<|message_id|>" && data.metadata) { - setAnswerIds(prev => { - const crntAnswerIds: number[] = Object.assign([], prev) - if (crntAnswerIds.length === nrOfQuestions) { - crntAnswerIds[nrOfQuestions - 1] = data.metadata; - } else { - crntAnswerIds.push(data.metadata); - } - - return crntAnswerIds; - }); - } else { - setAnswers(prev => { - const crntAnswers: string[] = Object.assign([], prev) - if (crntAnswers.length === nrOfQuestions) { - crntAnswers[nrOfQuestions - 1] = crntAnswers[nrOfQuestions - 1] + chunk; - } else { - crntAnswers.push(chunk); - } - - return crntAnswers; - }); - } - } }); - }, [company, chatId, questions]); + + setLoading(false); + + if (!response.ok) { + return; + } + + const data: { + answer: string; + answerId: number; + sources: string[]; + } = await response.json(); + + if (!data.answer || !data.answerId) { + return; + } + + setSources(prev => { + const metadata = [...new Set(data.sources || [])] as string[] + const crntSources: string[][] = Object.assign([], prev) + if (crntSources.length === nrOfQuestions) { + crntSources[nrOfQuestions - 1] = metadata; + } else { + crntSources.push(metadata); + } + + return crntSources; + }); + + setAnswerIds(prev => { + const crntAnswerIds: number[] = Object.assign([], prev) + if (crntAnswerIds.length === nrOfQuestions) { + crntAnswerIds[nrOfQuestions - 1] = data.answerId; + } else { + crntAnswerIds.push(data.answerId); + } + + return crntAnswerIds; + }); + + setAnswers(prev => { + const crntAnswers: string[] = Object.assign([], prev) + if (crntAnswers.length === nrOfQuestions) { + crntAnswers[nrOfQuestions - 1] = data.answer; + } else { + crntAnswers.push(data.answer); + } + + return crntAnswers; + }); + }, [company, chatId]); useEffect(() => { init(); @@ -161,7 +149,7 @@ export const Chatbot: React.FunctionComponent = ({ }: React.Props } - + Chatbot metrics diff --git a/src/dashboardWebView/components/Chatbot/Feedback.tsx b/src/dashboardWebView/components/Chatbot/Feedback.tsx index 4785bc57..429054b8 100644 --- a/src/dashboardWebView/components/Chatbot/Feedback.tsx +++ b/src/dashboardWebView/components/Chatbot/Feedback.tsx @@ -28,13 +28,14 @@ export const Feedback: React.FunctionComponent = ({ }, []); const callVote = useCallback(async (vote: boolean) => { - await fetch(`${aiUrl}/updateMessageRating/${answerId}`, { + await fetch(`${aiUrl}/api/ai-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - ratingValue: vote ? 1 : -1, + answerId, + vote: vote ? 1 : -1, }) }) }, [answerId]) diff --git a/src/dashboardWebView/components/Chatbot/QuestionAnswer.tsx b/src/dashboardWebView/components/Chatbot/QuestionAnswer.tsx index 7417daf2..4845367a 100644 --- a/src/dashboardWebView/components/Chatbot/QuestionAnswer.tsx +++ b/src/dashboardWebView/components/Chatbot/QuestionAnswer.tsx @@ -10,7 +10,7 @@ export interface IQuestionAnswerProps { export const QuestionAnswer: React.FunctionComponent = ({ question, answer, answerId, sources }: React.PropsWithChildren) => { return ( -
    +
    • {question}
    • diff --git a/src/dashboardWebView/providers/SettingsProvider.tsx b/src/dashboardWebView/providers/SettingsProvider.tsx index 4c47fe10..cbbe0894 100644 --- a/src/dashboardWebView/providers/SettingsProvider.tsx +++ b/src/dashboardWebView/providers/SettingsProvider.tsx @@ -4,19 +4,17 @@ interface ISettingsProviderProps { version?: string; experimental?: boolean; aiUrl?: string; - aiKey?: string; } const SettingsContext = React.createContext(undefined); -const SettingsProvider: React.FunctionComponent = ({ version, experimental, aiKey, aiUrl, children }: React.PropsWithChildren) => { +const SettingsProvider: React.FunctionComponent = ({ version, experimental, aiUrl, children }: React.PropsWithChildren) => { return ( {children}