Skip to content

Commit

Permalink
Add description AI + put theme suppor in stable mode
Browse files Browse the repository at this point in the history
  • Loading branch information
estruyf committed Jul 10, 2023
1 parent d52fabc commit 6de325b
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 18 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
7 changes: 2 additions & 5 deletions src/dashboardWebView/hooks/useThemeColors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 3 additions & 7 deletions src/dashboardWebView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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(
Expand Down
70 changes: 68 additions & 2 deletions src/listeners/panel/DataListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ 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';
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;

Expand Down Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions src/listeners/panel/TaxonomyListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class TaxonomyListener extends BaseListener {
requestId,
error: 'No article data'
} as MessageHandlerData<string>);
return;
}

panel.getWebview()?.postMessage({
Expand Down
1 change: 1 addition & 0 deletions src/panelWebView/CommandToCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
52 changes: 49 additions & 3 deletions src/panelWebView/components/Fields/TextField.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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>}>
Expand Down
2 changes: 2 additions & 0 deletions src/panelWebView/components/Fields/WrapperField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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>
);
Expand Down
2 changes: 2 additions & 0 deletions src/panelWebView/components/TagPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
sendUpdate(uniqValues);
setInputValue('');
}
}).catch(() => {
setLoading(false);
});
}, [selected]);

Expand Down
1 change: 1 addition & 0 deletions src/panelWebView/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ button {
/* Metadata section - Content type */
.metadata_field {
margin-bottom: 1rem;
position: relative;
}

.metadata_field__label {
Expand Down
41 changes: 40 additions & 1 deletion src/services/SponsorAI.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6de325b

Please sign in to comment.