From d52fabc9e9f76b768f64153bbe7e28b333b9c906 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 10 Jul 2023 12:02:38 +0200 Subject: [PATCH 1/7] Small css fix --- src/panelWebView/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index abc01eb4..d6f8d45d 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -294,6 +294,10 @@ button { justify-content: space-between; margin-bottom: 0.5rem; + &.metadata_field__alert { + justify-content: flex-start; + } + div { display: flex; align-items: center; From 6de325b8ec54f00c602e8cefb79885b4fe963f6a Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 10 Jul 2023 16:42:08 +0200 Subject: [PATCH 2/7] Add description AI + put theme suppor in stable mode --- CHANGELOG.md | 7 ++ src/dashboardWebView/hooks/useThemeColors.tsx | 7 +- src/dashboardWebView/index.tsx | 10 +-- src/listeners/panel/DataListener.ts | 70 ++++++++++++++++++- src/listeners/panel/TaxonomyListener.ts | 1 + src/panelWebView/CommandToCode.ts | 1 + .../components/Fields/TextField.tsx | 52 +++++++++++++- .../components/Fields/WrapperField.tsx | 2 + src/panelWebView/components/TagPicker.tsx | 2 + src/panelWebView/styles.css | 1 + src/services/SponsorAI.ts | 41 ++++++++++- 11 files changed, 176 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3a9b86..89d27de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,15 @@ ## [8.5.0] - 2023-xx-xx +### 🧪 Experimental features + +- External UI script support for dashboards +- Front matter AI 🤖 + ### ✨ New features +- Added description AI suggestion for GitHub sponsors +- The Visual Studio Code theme support is now released in the stable version - [#424](https://github.com/estruyf/vscode-front-matter/issues/424): Snippet wrapping to allow easier updates or changes to previously set snippets in the content - [#585](https://github.com/estruyf/vscode-front-matter/issues/585): New content relationship field type (`contentRelationship`) diff --git a/src/dashboardWebView/hooks/useThemeColors.tsx b/src/dashboardWebView/hooks/useThemeColors.tsx index 491f8205..7a4fe2af 100644 --- a/src/dashboardWebView/hooks/useThemeColors.tsx +++ b/src/dashboardWebView/hooks/useThemeColors.tsx @@ -7,11 +7,8 @@ export default function useThemeColors() { const { experimental } = useSettingsContext(); const getColors = useCallback((defaultColors: string, themeColors: string) => { - if (experimental) { - return themeColors; - } - - return defaultColors; + // The feature is now enabled by default + return themeColors; }, [experimental]); return { diff --git a/src/dashboardWebView/index.tsx b/src/dashboardWebView/index.tsx index 1c9c91c9..b793a356 100644 --- a/src/dashboardWebView/index.tsx +++ b/src/dashboardWebView/index.tsx @@ -107,10 +107,8 @@ if (elm) { const url = elm?.getAttribute('data-url'); const experimental = elm?.getAttribute('data-experimental'); - if (experimental) { - updateCssVariables(); - mutationObserver.observe(document.body, { childList: false, attributes: true }); - } + updateCssVariables(); + mutationObserver.observe(document.body, { childList: false, attributes: true }); if (isProd === 'true') { Sentry.init({ @@ -127,9 +125,7 @@ if (elm) { }); } - if (experimental) { - elm.setAttribute("class", "experimental bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]"); - } + elm.setAttribute("class", `${experimental ? "experimental" : ""} bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`); if (type === 'preview') { render( diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index a5567598..902e47e9 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -5,13 +5,14 @@ import { Folders } from '../../commands/Folders'; import { Command } from '../../panelWebView/Command'; import { CommandToCode } from '../../panelWebView/CommandToCode'; import { BaseListener } from './BaseListener'; -import { commands, ThemeIcon, window } from 'vscode'; -import { ArticleHelper, ContentType, Logger, Settings } from '../../helpers'; +import { authentication, commands, ThemeIcon, window } from 'vscode'; +import { ArticleHelper, ContentType, Extension, Logger, Settings } from '../../helpers'; import { COMMAND_NAME, DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FORMAT, + SETTING_SEO_TITLE_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from '../../constants'; import { Article, Preview } from '../../commands'; @@ -19,6 +20,9 @@ import { ParsedFrontMatter } from '../../parsers'; import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper'; import { Field, PostMessageData } from '../../models'; import { encodeEmoji } from '../../utils'; +import { ExplorerView } from '../../explorerView/ExplorerView'; +import { MessageHandlerData } from '@estruyf/vscode'; +import { SponsorAi } from '../../services/SponsorAI'; const FILE_LIMIT = 10; @@ -68,9 +72,71 @@ export class DataListener extends BaseListener { case CommandToCode.getDataEntries: this.getDataFileEntries(msg.command, msg.requestId || '', msg.payload); break; + case CommandToCode.aiSuggestDescription: + this.aiSuggestTaxonomy(msg.command, msg.requestId); + break; } } + private static async aiSuggestTaxonomy(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } + + const extPath = Extension.getInstance().extensionPath; + const panel = ExplorerView.getInstance(extPath); + + const editor = window.activeTextEditor; + if (!editor) { + panel.getWebview()?.postMessage({ + command, + requestId, + error: 'No active editor' + } as MessageHandlerData); + return; + } + + const article = ArticleHelper.getFrontMatter(editor); + if (!article || !article.data) { + panel.getWebview()?.postMessage({ + command, + requestId, + error: 'No article data' + } as MessageHandlerData); + return; + } + + const githubAuth = await authentication.getSession('github', ['read:user'], { silent: true }); + if (!githubAuth || !githubAuth.accessToken) { + return; + } + + const titleField = (Settings.get(SETTING_SEO_TITLE_FIELD) as string) || DefaultFields.Title; + + const suggestion = await SponsorAi.getDescription( + githubAuth.accessToken, + article.data[titleField] || '', + article.content || '' + ); + + console.log(suggestion); + + if (!suggestion) { + panel.getWebview()?.postMessage({ + command, + requestId, + error: 'No article data' + } as MessageHandlerData); + return; + } + + panel.getWebview()?.postMessage({ + command, + requestId, + payload: suggestion || [] + } as MessageHandlerData); + } + /** * Retrieve the information about the registered folders and its files */ diff --git a/src/listeners/panel/TaxonomyListener.ts b/src/listeners/panel/TaxonomyListener.ts index 208a6268..24d768e4 100644 --- a/src/listeners/panel/TaxonomyListener.ts +++ b/src/listeners/panel/TaxonomyListener.ts @@ -115,6 +115,7 @@ export class TaxonomyListener extends BaseListener { requestId, error: 'No article data' } as MessageHandlerData); + return; } panel.getWebview()?.postMessage({ diff --git a/src/panelWebView/CommandToCode.ts b/src/panelWebView/CommandToCode.ts index 6e654e98..4896c660 100644 --- a/src/panelWebView/CommandToCode.ts +++ b/src/panelWebView/CommandToCode.ts @@ -41,6 +41,7 @@ export enum CommandToCode { generateSlug = 'generate-slug', stopServer = 'stop-server', aiSuggestTaxonomy = 'ai-suggest-taxonomy', + aiSuggestDescription = 'ai-suggest-description', searchByType = 'search-by-type', processMediaData = 'process-media-data' } diff --git a/src/panelWebView/components/Fields/TextField.tsx b/src/panelWebView/components/Fields/TextField.tsx index 04ade9c5..601b7f95 100644 --- a/src/panelWebView/components/Fields/TextField.tsx +++ b/src/panelWebView/components/Fields/TextField.tsx @@ -1,17 +1,21 @@ -import { PencilIcon } from '@heroicons/react/outline'; +import { PencilIcon, SparklesIcon } from '@heroicons/react/outline'; import * as React from 'react'; import { useCallback, useEffect, useMemo } from 'react'; import { useRecoilState } from 'recoil'; -import { BaseFieldProps } from '../../../models'; +import { BaseFieldProps, PanelSettings } from '../../../models'; import { RequiredFieldsAtom } from '../../state'; import { FieldTitle } from './FieldTitle'; import { FieldMessage } from './FieldMessage'; +import { messageHandler } from '@estruyf/vscode/dist/client'; +import { CommandToCode } from '../../CommandToCode'; export interface ITextFieldProps extends BaseFieldProps { singleLine: boolean | undefined; wysiwyg: boolean | undefined; limit: number | undefined; rows?: number; + name: string; + settings: PanelSettings; onChange: (txtValue: string) => void; } @@ -25,11 +29,14 @@ export const TextField: React.FunctionComponent = ({ description, value, rows, + name, + settings, onChange, required }: React.PropsWithChildren) => { const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom); const [text, setText] = React.useState(value); + const [loading, setLoading] = React.useState(false); const onTextChange = (txtValue: string) => { setText(txtValue); @@ -75,6 +82,37 @@ export const TextField: React.FunctionComponent = ({ } }, [showRequiredState, isValid]); + const suggestDescription = () => { + setLoading(true); + messageHandler.request(CommandToCode.aiSuggestDescription).then((suggestion) => { + setLoading(false); + + if (suggestion) { + setText(suggestion); + onChange(suggestion); + } + }).catch(() => { + setLoading(false); + }); + }; + + const actionElement = useMemo(() => { + if (!settings?.aiEnabled || settings.seo.descriptionField !== name) { + return; + } + + return ( + + ); + }, [settings?.aiEnabled, name]); + useEffect(() => { if (text !== value) { setText(value); @@ -83,7 +121,15 @@ export const TextField: React.FunctionComponent = ({ return (
- } required={required} /> + { + loading && ( +
+ Generating suggestion... +
+ ) + } + + } required={required} /> {wysiwyg ? ( Loading field
}> diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 13f6a0a5..dd6cdd09 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -209,6 +209,7 @@ export const WrapperField: React.FunctionComponent = ({ return ( = ({ onChange={(value) => onSendUpdate(field.name, value, parentFields)} value={(fieldValue as string) || null} required={!!field.required} + settings={settings} /> ); diff --git a/src/panelWebView/components/TagPicker.tsx b/src/panelWebView/components/TagPicker.tsx index 2a5f775d..3157afa5 100644 --- a/src/panelWebView/components/TagPicker.tsx +++ b/src/panelWebView/components/TagPicker.tsx @@ -226,6 +226,8 @@ const TagPicker: React.FunctionComponent = ({ sendUpdate(uniqValues); setInputValue(''); } + }).catch(() => { + setLoading(false); }); }, [selected]); diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index d6f8d45d..d1203b9a 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -286,6 +286,7 @@ button { /* Metadata section - Content type */ .metadata_field { margin-bottom: 1rem; + position: relative; } .metadata_field__label { diff --git a/src/services/SponsorAI.ts b/src/services/SponsorAI.ts index 0c915d37..bd8b4b35 100644 --- a/src/services/SponsorAI.ts +++ b/src/services/SponsorAI.ts @@ -1,4 +1,4 @@ -import { SETTING_SEO_TITLE_LENGTH } from '../constants'; +import { SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from '../constants'; import { Logger, Notifications, Settings, TaxonomyHelper } from '../helpers'; import fetch from 'node-fetch'; import { TagType } from '../panelWebView/TagType'; @@ -47,6 +47,45 @@ export class SponsorAi { } } + public static async getDescription(token: string, title: string, content: string) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => { + Notifications.warning(`The AI title generation took too long. Please try again later.`); + controller.abort(); + }, 10000); + const signal = controller.signal; + + let articleContent = content; + if (articleContent.length > 2000) { + articleContent = articleContent.substring(0, 2000); + } + + const response = await fetch(`${AI_URL}/description`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json' + }, + body: JSON.stringify({ + title: title, + content: articleContent, + token: token, + nrOfCharacters: Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) || 160 + }), + signal: signal as any + }); + clearTimeout(timeout); + + const data: string = await response.text(); + + return data || ''; + } catch (e) { + Logger.error(`Sponsor AI: ${(e as Error).message}`); + return undefined; + } + } + /** * Get taxonomy suggestions from the AI * @param token From 4b87430776fd170ff22e9b7ea1fc8bd2aebf03db Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 11 Jul 2023 10:08:14 +0200 Subject: [PATCH 3/7] Fix hardcoded reference --- src/commands/Folders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 2564bf98..8c3701b9 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -182,7 +182,7 @@ export class Folders { ) { staticFolder = staticFolder === '/' || staticFolder === './' - ? Folders.getAbsFilePath('[[workspace]]') + ? Folders.getAbsFilePath(WORKSPACE_PLACEHOLDER) : Folders.getAbsFilePath(staticFolder); const wsFolder = Folders.getWorkspaceFolder(); if (wsFolder) { From 8d72f0845a16c27a666b27ba6fc288ba68d972f0 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 11 Jul 2023 10:53:08 +0200 Subject: [PATCH 4/7] Feedback: Hugo with partial content in modules vs. Dashboard #602 --- CHANGELOG.md | 1 + src/commands/Folders.ts | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d27de8..359bb4ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - [#591](https://github.com/estruyf/vscode-front-matter/issues/591): Support for date format in the `datetime` field - [#593](https://github.com/estruyf/vscode-front-matter/issues/593): Add support for date formatting in the preview path - [#599](https://github.com/estruyf/vscode-front-matter/issues/599): Add a placeholder when the base panel view is empty +- [#602](https://github.com/estruyf/vscode-front-matter/issues/602): Find content outside the Front Matter workspace folder ### ⚡️ Optimizations diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 8c3701b9..b4e86f3b 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -272,7 +272,8 @@ export class Folders { for (const folder of folders) { try { const folderPath = parseWinPath(folder.path); - let projectStart = parseWinPath(folder.path).replace(wsFolder, ''); + // let projectStart = parseWinPath(folder.path).replace(wsFolder, ''); + let projectStart = folderPath; if (typeof projectStart === 'string') { projectStart = projectStart.replace(/\\/g, '/'); @@ -291,7 +292,8 @@ export class Folders { filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`; } - let foundFiles = await workspace.findFiles(filePath, '**/node_modules/**'); + let foundFiles = await Folders.findFiles(filePath); + // Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces) foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath)); @@ -461,6 +463,11 @@ export class Folders { const isWindows = process.platform === 'win32'; let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || '')); absPath = isWindows ? absPath.split('/').join('\\') : absPath; + + if (absPath.includes('../')) { + absPath = join(absPath); + } + return parseWinPath(absPath); } @@ -577,4 +584,18 @@ export class Folders { }); }); } + + /** + * Find all files + * @param pattern + * @returns + */ + private static async findFiles(pattern: string): Promise { + return new Promise((resolve) => { + glob(pattern, { ignore: '**/node_modules/**' }, (err, files) => { + const allFiles = files.map((file) => Uri.file(file)); + resolve(allFiles); + }); + }); + } } From dee11aedced94a3113a69941c7a480b76b056043 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 11 Jul 2023 14:11:17 +0200 Subject: [PATCH 5/7] Slash fix --- src/commands/Folders.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index b4e86f3b..ee49fd45 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -272,23 +272,18 @@ export class Folders { for (const folder of folders) { try { const folderPath = parseWinPath(folder.path); - // let projectStart = parseWinPath(folder.path).replace(wsFolder, ''); - let projectStart = folderPath; - - if (typeof projectStart === 'string') { - projectStart = projectStart.replace(/\\/g, '/'); - projectStart = projectStart.startsWith('/') ? projectStart.substring(1) : projectStart; + if (typeof folderPath === 'string') { let files: Uri[] = []; for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) { let filePath = join( - projectStart, + folderPath, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}` ); - if (projectStart === '' && folder.excludeSubdir) { + if (folderPath === '' && folder.excludeSubdir) { filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`; } From edb8bf8ff78328aff9ac9197be88fa9058a81f01 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 11 Jul 2023 14:20:15 +0200 Subject: [PATCH 6/7] #602: fix for Windows paths --- src/commands/Folders.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index b4e86f3b..c749dc65 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -462,12 +462,13 @@ export class Folders { private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) { const isWindows = process.platform === 'win32'; let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || '')); - absPath = isWindows ? absPath.split('/').join('\\') : absPath; if (absPath.includes('../')) { absPath = join(absPath); } + absPath = isWindows ? absPath.split('/').join('\\') : absPath; + return parseWinPath(absPath); } From e32813847f67d8170a0b13d3aad10e7102e38a00 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 12 Jul 2023 09:22:57 +0200 Subject: [PATCH 7/7] Issue: Can't previe index.md of hugo reaf bundle with "previewPath": "/{{pathToken.relPath}}" #603 --- CHANGELOG.md | 1 + src/commands/Preview.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 359bb4ce..36536706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - [#590](https://github.com/estruyf/vscode-front-matter/issues/590): Fix for image fields inside a sub-block - [#595](https://github.com/estruyf/vscode-front-matter/issues/595): Fix for media metadata now showing up - [#596](https://github.com/estruyf/vscode-front-matter/issues/596): Fix for number field in block data +- [#603](https://github.com/estruyf/vscode-front-matter/issues/603): Fix problem with page bundles and path placeholders ## [8.4.0] - 2023-04-03 - [Release notes](https://beta.frontmatter.codes/updates/v8.4.0) diff --git a/src/commands/Preview.ts b/src/commands/Preview.ts index ec492cd9..59053fa7 100644 --- a/src/commands/Preview.ts +++ b/src/commands/Preview.ts @@ -11,7 +11,7 @@ import { SETTING_DATE_FORMAT } from './../constants'; import { ArticleHelper } from './../helpers/ArticleHelper'; -import { join } from 'path'; +import { join, parse } from 'path'; import { commands, env, Uri, ViewColumn, window, WebviewPanel } from 'vscode'; import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers'; import { ContentFolder, ContentType, PreviewSettings } from '../models'; @@ -290,6 +290,11 @@ export class Preview { const folderPath = wsFolder ? parseWinPath(wsFolder.fsPath) : ''; const relativePath = filePath.replace(folderPath, ''); pathname = processPathPlaceholders(pathname, relativePath, filePath, selectedFolder); + + const file = parse(filePath); + if (file.name.toLowerCase() === 'index' && pathname.endsWith(slug)) { + slug = ''; + } } // Support front matter placeholders - {{fm.}}