Merge branch 'poc/ai' into dev

This commit is contained in:
Elio Struyf
2023-03-09 21:06:25 +01:00
32 changed files with 2496 additions and 225 deletions
+2
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#C5C5C5" d="M16 19a6.99 6.99 0 0 1-5.833-3.129l1.666-1.107a5 5 0 0 0 8.334 0l1.666 1.107A6.99 6.99 0 0 1 16 19zm4-11a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2zm-8 0a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2z"/>
<path fill="#C5C5C5" d="M17.736 30L16 29l4-7h6a1.997 1.997 0 0 0 2-2V6a1.997 1.997 0 0 0-2-2H6a1.997 1.997 0 0 0-2 2v14a1.997 1.997 0 0 0 2 2h9v2H6a4 4 0 0 1-4-4V6a3.999 3.999 0 0 1 4-4h20a3.999 3.999 0 0 1 4 4v14a4 4 0 0 1-4 4h-4.835Z"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#424242" d="M16 19a6.99 6.99 0 0 1-5.833-3.129l1.666-1.107a5 5 0 0 0 8.334 0l1.666 1.107A6.99 6.99 0 0 1 16 19zm4-11a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2zm-8 0a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2z"/>
<path fill="#424242" d="M17.736 30L16 29l4-7h6a1.997 1.997 0 0 0 2-2V6a1.997 1.997 0 0 0-2-2H6a1.997 1.997 0 0 0-2 2v14a1.997 1.997 0 0 0 2 2h9v2H6a4 4 0 0 1-4-4V6a3.999 3.999 0 0 1 4-4h20a3.999 3.999 0 0 1 4 4v14a4 4 0 0 1-4 4h-4.835Z"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

+1608 -11
View File
File diff suppressed because it is too large Load Diff
+149 -156
View File
@@ -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"
@@ -1868,6 +1850,15 @@
"title": "Preview content",
"category": "Front Matter"
},
{
"command": "frontMatter.chatbot",
"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",
"title": "Promote settings from local to team level",
@@ -1923,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"
@@ -2005,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"
@@ -2030,8 +2020,7 @@
"group": "frontmatter@3"
}
],
"commandPalette": [
{
"commandPalette": [{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2172,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:*",
@@ -2270,6 +2258,7 @@
"@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",
@@ -2300,6 +2289,7 @@
"@webpack-cli/serve": "^1.6.0",
"ajv": "^8.8.2",
"array-move": "^4.0.0",
"assert": "^2.0.0",
"autoprefixer": "^10.4.13",
"chai": "^4.3.6",
"css-loader": "5.2.7",
@@ -2331,16 +2321,19 @@
"postcss-loader": "^7.0.2",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"process": "^0.11.10",
"react": "17.0.1",
"react-datepicker": "4.2.1",
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"react-markdown": "^8.0.5",
"react-popper": "^2.3.0",
"react-quill": "^2.0.0-beta.4",
"react-router-dom": "^6.3.0",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.1.0",
"recoil": "^0.4.1",
"remark-gfm": "^3.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"simple-git": "^3.10.0",
@@ -2365,4 +2358,4 @@
"vsce": {
"dependencies": false
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import { Telemetry } from './../helpers/Telemetry';
import { TelemetryEvent, PreviewCommands, SETTING_EXPERIMENTAL } from './../constants';
import { join } from 'path';
import { commands, Uri, ViewColumn, window } from 'vscode';
import { Extension, Settings } from '../helpers';
import { WebviewHelper } from '@estruyf/vscode';
export class Chatbot {
/**
* Open the Chatbot in the editor
*/
public static async open(extensionPath: string) {
// Create the preview webview
const webView = window.createWebviewPanel(
'frontMatterChatbot',
'Front Matter AI - Ask me anything',
{
viewColumn: ViewColumn.Beside,
preserveFocus: true
},
{
enableScripts: true
}
);
webView.iconPath = {
dark: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-light.svg'))
};
const cspSource = webView.webview.cspSource;
webView.webview.onDidReceiveMessage((message) => {
switch (message.command) {
case PreviewCommands.toVSCode.open:
if (message.data) {
commands.executeCommand('vscode.open', message.data);
}
return;
}
});
const dashboardFile = 'dashboardWebView.js';
const localPort = `9000`;
const localServerUrl = `localhost:${localPort}`;
const nonce = WebviewHelper.getNonce();
const ext = Extension.getInstance();
const isProd = ext.isProductionMode;
const version = ext.getVersion();
const isBeta = ext.isBetaVersion();
const extensionUri = ext.extensionPath;
const csp = [
`default-src 'none';`,
`img-src ${cspSource} http: https:;`,
`script-src ${
isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`
} 'unsafe-eval'`,
`style-src ${cspSource} 'self' 'unsafe-inline' http: https:`,
`connect-src https://* ${
isProd
? ``
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
}`
];
let scriptUri = '';
if (isProd) {
scriptUri = webView.webview
.asWebviewUri(Uri.joinPath(extensionUri, 'dist', dashboardFile))
.toString();
} else {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
// Get experimental setting
const experimental = Settings.get(SETTING_EXPERIMENTAL);
webView.webview.html = `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter Docs Chatbot</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
<div id="app" data-type="chatbot" data-isProd="${isProd}" data-environment="${
isBeta ? 'BETA' : 'main'
}" data-version="${version.usedVersion}" ${
experimental ? `data-experimental="${experimental}"` : ''
} style="width:100%;height:100%;margin:0;padding:0;"></div>
<script ${isProd ? `nonce="${nonce}"` : ''} src="${scriptUri}"></script>
</body>
</html>
`;
Telemetry.send(TelemetryEvent.openChatbot);
}
}
+1
View File
@@ -1,6 +1,7 @@
export * from './Article';
export * from './Backers';
export * from './Cache';
export * from './Chatbot';
export * from './Content';
export * from './Dashboard';
export * from './Diagnostics';
+14
View File
@@ -0,0 +1,14 @@
import * as React from 'react';
export interface IChatIconProps {
className?: string;
}
export const ChatIcon: React.FunctionComponent<IChatIconProps> = ({ className }: React.PropsWithChildren<IChatIconProps>) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" className={className || 'h-4 w-4'} viewBox="0 0 32 32">
<path fill="currentcolor" d="M16 19a6.99 6.99 0 0 1-5.833-3.129l1.666-1.107a5 5 0 0 0 8.334 0l1.666 1.107A6.99 6.99 0 0 1 16 19zm4-11a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2zm-8 0a2 2 0 1 0 2 2a1.98 1.98 0 0 0-2-2z" />
<path fill="currentcolor" d="M17.736 30L16 29l4-7h6a1.997 1.997 0 0 0 2-2V6a1.997 1.997 0 0 0-2-2H6a1.997 1.997 0 0 0-2 2v14a1.997 1.997 0 0 0 2 2h9v2H6a4 4 0 0 1-4-4V6a3.999 3.999 0 0 1 4-4h20a3.999 3.999 0 0 1 4 4v14a4 4 0 0 1-4 4h-4.835Z" />
</svg>
);
};
+1
View File
@@ -28,6 +28,7 @@ export const COMMAND_NAME = {
initTemplate: getCommandName('initTemplate'),
collapseSections: getCommandName('collapseSections'),
preview: getCommandName('preview'),
chatbot: getCommandName('chatbot'),
dashboard: getCommandName('dashboard'),
dashboardMedia: getCommandName('dashboard.media'),
dashboardSnippets: getCommandName('dashboard.snippets'),
+3
View File
@@ -28,6 +28,9 @@ export const TelemetryEvent = {
updateMediaMetadata: 'updateMediaMetadata',
openExplorerView: 'openExplorerView',
// Chatbot
openChatbot: 'openChatbot',
// Content types
generateContentType: 'generateContentType',
addMissingFields: 'addMissingFields',
@@ -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<IAnswerProps> = ({
answer,
answerId,
sources,
}: React.PropsWithChildren<IAnswerProps>) => {
if (!answer) {
return null;
}
return (
<li className='answer'>
<div className='text-lg flex justify-between'>
<p>Answer</p>
<Feedback answerId={answerId} />
</div>
<ReactMarkdown children={answer} remarkPlugins={[remarkGfm]} />
{
sources && sources.length > 0 && (
<div>
<p className='text-lg'>Resources</p>
<ul className={`space-y-2 list-disc pl-4`}>
{sources.map((source, idx) => (
<li key={`source-${idx}`} className={`text-sm`}>
<a className={`text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]`} href={source} target="_blank" rel="noreferrer">{source}</a>
</li>
))}
</ul>
</div>
)
}
<div className={`-mx-4 -mb-4 py-2 px-4 bg-[var(--vscode-sideBar-background)] text-[var(--vscode-sideBarTitle-foreground)] text-xs rounded-b`} style={{
fontFamily: "var(--vscode-editor-font-family)",
}}>
Warning: Anwers might be wrong. In case of doubt, please consult the docs.
</div>
</li>
);
};
@@ -0,0 +1,169 @@
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';
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<IChatbotProps> = ({ }: React.PropsWithChildren<IChatbotProps>) => {
const { aiKey, aiUrl } = useSettingsContext();
const [company, setCompany] = React.useState<string | undefined>(undefined);
const [chatId, setChatId] = React.useState<number | undefined>(undefined);
const [questions, setQuestions] = React.useState<string[]>([]);
const [answers, setAnswers] = React.useState<string[]>([]);
const [answerIds, setAnswerIds] = React.useState<number[]>([]);
const [sources, setSources] = React.useState<string[][]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
const init = async () => {
setLoading(true);
const initResponse = await fetch(`${aiUrl}/initializeMendable`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
anon_key: aiKey
})
});
if (!initResponse.ok) {
return;
}
const initJson: InitResponse = 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) {
return;
}
const newChat: NewConversationResponse = await newChatResponse.json();
setCompany(initJson.company.name);
setChatId(newChat.conversation_id);
setLoading(false);
};
const callChatbot = useCallback(async (message) => {
const nrOfQuestions = questions.length + 1;
setLoading(true);
setQuestions(prev => [...prev, message])
setAnswers(prev => [...prev, ""])
setSources(prev => [...prev, []])
setAnswerIds(prev => [...prev, 0])
if (!company || !chatId) {
return;
}
await fetchEventSource(`${aiUrl}/qaChat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
},
body: JSON.stringify({
company: company,
conversation_id: chatId,
history: [{ prompt: "", response: "", sources: [] }],
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]);
useEffect(() => {
init();
}, []);
return (
<div className={`flex flex-col overflow-x-hidden h-full w-full items-center overflow-hidden`}>
<Header />
<div className='w-full h-[1px] bg-[var(--frontmatter-border)] -mx-4 mb-4'></div>
<main className={`qa__bot flex-grow w-full max-w-xl overflow-y-auto overflow-x-hidden`}>
{
questions && questions.length > 0 ? questions.map((question, idx) => (
<QuestionAnswer
key={idx}
question={question}
answer={answers[idx]}
answerId={answerIds[idx]}
sources={sources[idx]} />
)) : (
<Placeholder />
)
}
{
loading && (questions && questions.length > 0) && <Loader />
}
</main>
<Chatbox isLoading={loading} onTrigger={callChatbot} />
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=chatbot" alt="Chatbot metrics" />
</div>
);
};
@@ -0,0 +1,61 @@
import { PaperAirplaneIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import useThemeColors from '../../hooks/useThemeColors';
export interface IChatboxProps {
isLoading: boolean;
onTrigger: (message: string) => void;
}
export const Chatbox: React.FunctionComponent<IChatboxProps> = ({ isLoading, onTrigger }: React.PropsWithChildren<IChatboxProps>) => {
const [message, setMessage] = React.useState<string>("");
const [isFocussed, setIsFocussed] = React.useState<boolean>(false);
const { getColors } = useThemeColors();
const callAi = useCallback(() => {
setTimeout(() => {
setMessage("")
}, 0);
onTrigger(message);
}, [message]);
return (
<footer className={`w-full mt-4 bg-[var(--vscode-input-background)] border-t ${isFocussed ? "border-[var(--vscode-focusBorder)]" : "border-[var(--frontmatter-border)]"}`}>
<div className={`w-full max-w-xl relative pb-4 mx-auto`}>
<div className='chatbox px-4'>
<textarea
className={`
resize-none w-full outline-none border-0 pr-8
${getColors(
'focus:outline-none border-gray-300 text-vulcan-500',
'border-transparent bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] focus:outline-none focus:border-transparent'
)}`}
placeholder='How can I configure Front Matter?'
autoFocus={true}
value={message}
cols={30}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
callAi();
}
}}
onFocus={() => setIsFocussed(true)}
onBlur={() => setIsFocussed(false)}
/>
<button
className={`absolute right-6 top-3 text-[var(--frontmatter-button-background)] hover:text-[var(--frontmatter-button-hoverBackground)] disabled:opacity-50 disabled:text-[var(--vscode-disabledForeground)]`}
type='button'
disabled={message.trim().length === 0 || isLoading}
onClick={callAi}
>
<PaperAirplaneIcon className='h-6 w-6 rotate-90' />
</button>
</div>
</div>
</footer>
);
};
@@ -0,0 +1,72 @@
import * as React from 'react';
import { ThumbDownIcon, ThumbUpIcon } from '@heroicons/react/outline';
import { ThumbDownIcon as ThumbDownSolidIcon, ThumbUpIcon as ThumbUpSolidIcon } from '@heroicons/react/solid';
import { useCallback } from 'react';
import { useSettingsContext } from '../../providers/SettingsProvider';
export interface IFeedbackProps {
answerId: number;
}
export const Feedback: React.FunctionComponent<IFeedbackProps> = ({
answerId
}: React.PropsWithChildren<IFeedbackProps>) => {
const { aiUrl } = useSettingsContext();
const [isUpVoted, setIsUpVoted] = React.useState<boolean>(false);
const [isDownVoted, setIsDownVoted] = React.useState<boolean>(false);
const onUpVote = useCallback(() => {
setIsUpVoted(true)
setIsDownVoted(false)
callVote(true)
}, []);
const onDownVote = useCallback(() => {
setIsDownVoted(true)
setIsUpVoted(false)
callVote(false)
}, []);
const callVote = useCallback(async (vote: boolean) => {
await fetch(`${aiUrl}/updateMessageRating/${answerId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ratingValue: vote ? 1 : -1,
})
})
}, [answerId])
if (!answerId) {
return null;
}
return (
<div className='text-lg flex gap-4'>
<button
className='hover:text-[var(--vscode-textLink-activeForeground)]'
onClick={onUpVote}>
{
isUpVoted ? (
<ThumbUpSolidIcon className='h-4 w-4 text-[var(--vscode-textLink-foreground)]' />
) : (
<ThumbUpIcon className='h-4 w-4' />
)
}
</button>
<button
className='hover:text-[var(--vscode-textLink-activeForeground)]'
onClick={() => onDownVote()}>
{
isDownVoted ? (
<ThumbDownSolidIcon className='h-4 w-4 text-[var(--vscode-textLink-foreground)]' />
) : (
<ThumbDownIcon className='h-4 w-4' />
)
}
</button>
</div>
);
};
@@ -0,0 +1,23 @@
import * as React from 'react';
import { ChatIcon } from '../../../components/icons/ChatIcon';
export interface IHeaderProps { }
export const Header: React.FunctionComponent<IHeaderProps> = (props: React.PropsWithChildren<IHeaderProps>) => {
return (
<header className={`w-full max-w-xl m-4 px-4`}>
<h1 className='text-2xl flex items-center space-x-4'>
<ChatIcon className='h-6 w-6' />
<span>Ask Front Matter AI</span>
</h1>
<h2
className='mt-2 text-sm text-[var(--frontmatter-secondary-text)]'
style={{
fontFamily: "var(--vscode-editor-font-family)",
}}
>
Our AI, powered by <a className={`text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]`} href={`https://www.mendable.ai/`} title={`mendable.ai`}>mendable.ai</a>, has processed the documentation and can assist you with any queries regarding Front Matter. Go ahead and ask away!
</h2>
</header>
);
};
@@ -0,0 +1,15 @@
import * as React from 'react';
export interface ILoaderProps { }
export const Loader: React.FunctionComponent<ILoaderProps> = (props: React.PropsWithChildren<ILoaderProps>) => {
return (
<div>
<div className="mt-4 flex items-center justify-center space-x-2 animate-pulse">
<div className="w-4 h-4 bg-[var(--frontmatter-button-background)] rounded-full"></div>
<div className="w-4 h-4 bg-[var(--frontmatter-button-background)] rounded-full"></div>
<div className="w-4 h-4 bg-[var(--frontmatter-button-background)] rounded-full"></div>
</div>
</div>
);
};
@@ -0,0 +1,16 @@
import * as React from 'react';
import { ChatIcon } from '../../../components/icons/ChatIcon';
export interface IPlaceholderProps { }
export const Placeholder: React.FunctionComponent<IPlaceholderProps> = (props: React.PropsWithChildren<IPlaceholderProps>) => {
return (
<div className='w-full h-full flex flex-col gap-2 items-center justify-center text-[var(--frontmatter-secondary-text)] opacity-80'>
<ChatIcon className='h-16 w-16 text-[var(--vscode-sideBarTitle-foreground)]' />
<div className='text-[var(--vscode-sideBarTitle-foreground)] text-2xl mt-4'>
Start by asking a question
</div>
</div>
);
};
@@ -0,0 +1,22 @@
import * as React from 'react';
import { Answer } from './Answer';
export interface IQuestionAnswerProps {
question: string;
answer: string;
answerId: number;
sources: string[];
}
export const QuestionAnswer: React.FunctionComponent<IQuestionAnswerProps> = ({ question, answer, answerId, sources }: React.PropsWithChildren<IQuestionAnswerProps>) => {
return (
<ul className={`space-y-4 px-4`}>
<li className='question'>{question}</li>
<Answer
answer={answer}
answerId={answerId}
sources={sources} />
</ul>
);
};
@@ -0,0 +1,16 @@
export interface InitResponse {
company: Company;
project: Project;
}
export interface Project {
id: number;
created_at: string;
name: string;
company_id: number;
}
export interface Company {
company_id: number;
name: string;
}
@@ -0,0 +1,6 @@
export interface NewConversationResponse {
conversation_id: number;
start_time: string;
end_time?: any;
project_id: number;
}
@@ -43,6 +43,8 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=content" alt="Content metrics" />
</PageLayout>
);
};
@@ -317,6 +317,8 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (
/>
<ToastContainer />
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=dataview" alt="DataView metrics" />
</div >
);
};
@@ -272,6 +272,8 @@ export const Media: React.FunctionComponent<IMediaProps> = (
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=media" alt="Media metrics" />
</PageLayout>
);
};
@@ -84,6 +84,8 @@ export const Preview: React.FunctionComponent<IPreviewProps> = ({
marginTop: '30px'
}}
></iframe>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=preview" alt="Preview metrics" />
</div>
);
};
@@ -180,6 +180,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=snippets" alt="Snippets metrics" />
</PageLayout>
);
};
@@ -97,6 +97,8 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
version={settings?.versionInfo}
isBacker={settings?.isBacker}
/>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=taxonomy" alt="Taxonomy metrics" />
</PageLayout>
);
};
@@ -121,6 +121,8 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({
</h2>
</div>
</main>
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=welcome" alt="Welcome metrics" />
</div>
);
};
+10
View File
@@ -10,6 +10,7 @@ import './styles.css';
import { Preview } from './components/Preview';
import { SettingsProvider } from './providers/SettingsProvider';
import { CustomPanelViewResult } from '../models';
import { Chatbot } from './components/Chatbot/Chatbot';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
@@ -129,6 +130,15 @@ if (elm) {
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
<Preview url={url} />
</SettingsProvider>, elm);
} else if (type === 'chatbot') {
render(
<SettingsProvider
aiKey='466f5321-12d9-4d64-9e5b-ea5db41ed2ba'
aiUrl='https://aijsplayground-production.up.railway.app'
experimental={experimental === 'true'}
version={version || ""}>
<Chatbot />
</SettingsProvider>, elm);
} else {
render(
<RecoilRoot>
@@ -3,27 +3,20 @@ import * as React from 'react'
interface ISettingsProviderProps {
version?: string;
experimental?: boolean;
aiUrl?: string;
aiKey?: string;
}
const SettingsContext = React.createContext<ISettingsProviderProps | undefined>(undefined);
const SettingsProvider: React.FunctionComponent<ISettingsProviderProps> = ({ version, experimental, children }: React.PropsWithChildren<ISettingsProviderProps>) => {
const [ crntVersion, setCrntVersion ] = React.useState<string | undefined>(undefined);
const [ crntExprimental, setCrntExprimental ] = React.useState<boolean>(false);
React.useEffect(() => {
setCrntVersion(version);
}, [version]);
React.useEffect(() => {
setCrntExprimental(typeof experimental === 'boolean' ? experimental : false);
}, [experimental]);
const SettingsProvider: React.FunctionComponent<ISettingsProviderProps> = ({ version, experimental, aiKey, aiUrl, children }: React.PropsWithChildren<ISettingsProviderProps>) => {
return (
<SettingsContext.Provider
<SettingsContext.Provider
value={{
version: crntVersion,
experimental: crntExprimental
version,
experimental,
aiUrl,
aiKey
}}
>
{children}
+53
View File
@@ -401,3 +401,56 @@
}
}
}
.qa__bot {
> div {
@apply space-y-4;
}
li {
@apply space-y-2;
}
pre code {
white-space: pre-wrap;
}
.question {
@apply relative ml-auto mr-3 w-5/6 rounded-full rounded-br-none bg-teal-900 py-2 px-4 text-whisper-500;
&:after {
--size: 1rem;
content: '';
position: absolute;
bottom: 0;
height: var(--size);
width: var(--size);
z-index: 2;
right: calc(var(--size) * -1);
border-bottom-right-radius: 8rem;
background: radial-gradient(
circle at top right,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 0) var(--size),
#009aa3 var(--size)
);
}
}
.answer {
@apply break-all rounded border border-teal-100 border-opacity-20 p-4 pb-0;
}
}
.chatbox {
textarea {
border: 0;
outline: 0;
&:focus {
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
}
}
}
+8 -2
View File
@@ -26,7 +26,8 @@ import {
Dashboard,
Article,
Settings,
StatusListener
StatusListener,
Chatbot
} from './commands';
let frontMatterStatusBar: vscode.StatusBarItem;
@@ -309,6 +310,11 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath))
);
// Chat to the bot
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.chatbot, () => Chatbot.open(extensionPath))
);
// Inserting an image in Markdown
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.insertMedia, Article.insertMedia)
@@ -369,7 +375,7 @@ export async function activate(context: vscode.ExtensionContext) {
createFolder
);
console.log(`FRONT MATTER CMS activated!`)
console.log(`FRONT MATTER CMS activated!`);
}
export function deactivate() {
+59 -41
View File
@@ -3,52 +3,70 @@
'use strict';
const path = require('path');
const {
ProvidePlugin
} = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const config = [
{
name: 'dashboard',
target: 'web',
entry: './src/dashboardWebView/index.tsx',
output: {
filename: 'dashboardWebView.js',
path: path.resolve(__dirname, '../dist')
},
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
fallback: { "path": require.resolve("path-browserify") }
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [{
loader: 'ts-loader'
}]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
const config = [{
name: 'dashboard',
target: 'web',
entry: './src/dashboardWebView/index.tsx',
output: {
filename: 'dashboardWebView.js',
path: path.resolve(__dirname, '../dist')
},
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
fallback: {
"assert": require.resolve("assert"),
"path": require.resolve("path-browserify")
}
},
module: {
rules: [{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [{
loader: 'ts-loader'
}]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
resolve: {
fullySpecified: false
}
]
},
performance: {
hints: false
},
plugins: [],
devServer: {
compress: true,
port: 9000,
hot: true,
allowedHosts: "all",
headers: {
"Access-Control-Allow-Origin": "*",
}
]
},
performance: {
hints: false
},
plugins: [
new ProvidePlugin({
// Make a global `process` variable that points to the `process` package,
// because the `util` package expects there to be a global variable named `process`.
// Thanks to https://stackoverflow.com/a/65018686/14239942
process: 'process/browser'
})
],
devServer: {
compress: true,
port: 9000,
hot: true,
allowedHosts: "all",
headers: {
"Access-Control-Allow-Origin": "*",
}
}
];
}];
module.exports = (env, argv) => {
for (const configItem of config) {
@@ -56,7 +74,7 @@ module.exports = (env, argv) => {
if (argv.mode === 'production') {
configItem.devtool = "hidden-source-map";
configItem.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: "dashboard.html",