mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-26 21:11:38 +02:00
Merge branch 'dev' into issue/598
This commit is contained in:
@@ -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`)
|
||||
|
||||
@@ -18,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
|
||||
|
||||
@@ -32,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)
|
||||
|
||||
|
||||
+26
-9
@@ -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) {
|
||||
@@ -272,26 +272,23 @@ export class Folders {
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
const folderPath = parseWinPath(folder.path);
|
||||
let projectStart = parseWinPath(folder.path).replace(wsFolder, '');
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@@ -460,7 +457,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 || ''));
|
||||
|
||||
if (absPath.includes('../')) {
|
||||
absPath = join(absPath);
|
||||
}
|
||||
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
|
||||
return parseWinPath(absPath);
|
||||
}
|
||||
|
||||
@@ -577,4 +580,18 @@ export class Folders {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all files
|
||||
* @param pattern
|
||||
* @returns
|
||||
*/
|
||||
private static async findFiles(pattern: string): Promise<Uri[]> {
|
||||
return new Promise((resolve) => {
|
||||
glob(pattern, { ignore: '**/node_modules/**' }, (err, files) => {
|
||||
const allFiles = files.map((file) => Uri.file(file));
|
||||
resolve(allFiles);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.<field>}}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<string>);
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (!article || !article.data) {
|
||||
panel.getWebview()?.postMessage({
|
||||
command,
|
||||
requestId,
|
||||
error: 'No article data'
|
||||
} as MessageHandlerData<string>);
|
||||
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<string>);
|
||||
return;
|
||||
}
|
||||
|
||||
panel.getWebview()?.postMessage({
|
||||
command,
|
||||
requestId,
|
||||
payload: suggestion || []
|
||||
} as MessageHandlerData<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the information about the registered folders and its files
|
||||
*/
|
||||
|
||||
@@ -115,6 +115,7 @@ export class TaxonomyListener extends BaseListener {
|
||||
requestId,
|
||||
error: 'No article data'
|
||||
} as MessageHandlerData<string>);
|
||||
return;
|
||||
}
|
||||
|
||||
panel.getWebview()?.postMessage({
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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<ITextFieldProps> = ({
|
||||
description,
|
||||
value,
|
||||
rows,
|
||||
name,
|
||||
settings,
|
||||
onChange,
|
||||
required
|
||||
}: React.PropsWithChildren<ITextFieldProps>) => {
|
||||
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
|
||||
const [text, setText] = React.useState<string | null>(value);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
|
||||
const onTextChange = (txtValue: string) => {
|
||||
setText(txtValue);
|
||||
@@ -75,6 +82,37 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
}
|
||||
}, [showRequiredState, isValid]);
|
||||
|
||||
const suggestDescription = () => {
|
||||
setLoading(true);
|
||||
messageHandler.request<string>(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 (
|
||||
<button
|
||||
className='metadata_field__title__action'
|
||||
title={`Use Front Matter AI to suggest ${label?.toLowerCase()}`}
|
||||
type='button'
|
||||
onClick={() => suggestDescription()}
|
||||
disabled={loading}>
|
||||
<SparklesIcon />
|
||||
</button>
|
||||
);
|
||||
}, [settings?.aiEnabled, name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (text !== value) {
|
||||
setText(value);
|
||||
@@ -83,7 +121,15 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
|
||||
return (
|
||||
<div className={`metadata_field`}>
|
||||
<FieldTitle label={label} icon={<PencilIcon />} required={required} />
|
||||
{
|
||||
loading && (
|
||||
<div className='metadata_field__loading'>
|
||||
Generating suggestion...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<FieldTitle label={label} actionElement={actionElement} icon={<PencilIcon />} required={required} />
|
||||
|
||||
{wysiwyg ? (
|
||||
<React.Suspense fallback={<div>Loading field</div>}>
|
||||
|
||||
@@ -209,6 +209,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<TextField
|
||||
name={field.name}
|
||||
label={field.title || field.name}
|
||||
description={field.description}
|
||||
singleLine={field.single}
|
||||
@@ -218,6 +219,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
onChange={(value) => onSendUpdate(field.name, value, parentFields)}
|
||||
value={(fieldValue as string) || null}
|
||||
required={!!field.required}
|
||||
settings={settings}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
);
|
||||
|
||||
@@ -226,6 +226,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
|
||||
sendUpdate(uniqValues);
|
||||
setInputValue('');
|
||||
}
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [selected]);
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ button {
|
||||
/* Metadata section - Content type */
|
||||
.metadata_field {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata_field__label {
|
||||
@@ -294,6 +295,10 @@ button {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&.metadata_field__alert {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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<number>(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
|
||||
|
||||
Reference in New Issue
Block a user