mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
#823 - First steps into integrating GH Copilot
This commit is contained in:
@@ -431,6 +431,7 @@
|
||||
"panel.fields.slugField.generate": "Generate slug",
|
||||
|
||||
"panel.fields.textField.ai.message": "Use Front Matter AI to suggest {0}",
|
||||
"panel.fields.textField.copilot.message": "Use Copilot to suggest {0}",
|
||||
"panel.fields.textField.ai.generate": "Generating suggestion...",
|
||||
"panel.fields.textField.loading": "Loading field",
|
||||
"panel.fields.textField.limit": "Field limit reached {0}",
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/vscode": "^1.73.0",
|
||||
"@types/vscode": "^1.90.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@vscode-elements/elements": "^1.2.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"yawn-yaml": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.73.0"
|
||||
"vscode": "^1.90.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2084,9 +2084,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.86.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.86.0.tgz",
|
||||
"integrity": "sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==",
|
||||
"version": "1.90.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.90.0.tgz",
|
||||
"integrity": "sha512-oT+ZJL7qHS9Z8bs0+WKf/kQ27qWYR3trsXpq46YDjFqBsMLG4ygGGjPaJ2tyrH0wJzjOEmDyg9PDJBBhWg9pkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/vscode-webview": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"qna": "https://github.com/estruyf/vscode-front-matter/discussions",
|
||||
"engines": {
|
||||
"vscode": "^1.73.0"
|
||||
"vscode": "^1.90.0"
|
||||
},
|
||||
"l10n": "./l10n",
|
||||
"categories": [
|
||||
@@ -2779,7 +2779,7 @@
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/vscode": "^1.73.0",
|
||||
"@types/vscode": "^1.90.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@vscode-elements/elements": "^1.2.0",
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
TaxonomyType
|
||||
} from '../models';
|
||||
import { Folders } from '../commands';
|
||||
import { Copilot } from '../services/Copilot';
|
||||
|
||||
export class PanelSettings {
|
||||
public static async get(): Promise<IPanelSettings> {
|
||||
@@ -53,6 +54,7 @@ export class PanelSettings {
|
||||
try {
|
||||
return {
|
||||
aiEnabled: Settings.get<boolean>(SETTING_SPONSORS_AI_ENABLED) || false,
|
||||
copilotEnabled: await Copilot.isInstalled(),
|
||||
git: await GitListener.getSettings(),
|
||||
seo: {
|
||||
title: (Settings.get(SETTING_SEO_TITLE_LENGTH) as number) || -1,
|
||||
|
||||
@@ -5,7 +5,16 @@ import { Folders } from '../../commands/Folders';
|
||||
import { Command } from '../../panelWebView/Command';
|
||||
import { CommandToCode } from '../../panelWebView/CommandToCode';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { Uri, authentication, commands, window } from 'vscode';
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
LanguageModelChatMessage,
|
||||
LanguageModelChatResponse,
|
||||
Uri,
|
||||
authentication,
|
||||
commands,
|
||||
lm,
|
||||
window
|
||||
} from 'vscode';
|
||||
import {
|
||||
ArticleHelper,
|
||||
Extension,
|
||||
@@ -25,6 +34,7 @@ import {
|
||||
SETTING_DATE_FORMAT,
|
||||
SETTING_GLOBAL_ACTIVE_MODE,
|
||||
SETTING_GLOBAL_MODES,
|
||||
SETTING_SEO_DESCRIPTION_LENGTH,
|
||||
SETTING_SEO_TITLE_FIELD,
|
||||
SETTING_TAXONOMY_CONTENT_TYPES
|
||||
} from '../../constants';
|
||||
@@ -104,6 +114,84 @@ export class DataListener extends BaseListener {
|
||||
case CommandToCode.aiSuggestDescription:
|
||||
this.aiSuggestTaxonomy(msg.command, msg.requestId);
|
||||
break;
|
||||
case CommandToCode.copilotDescription:
|
||||
this.copilotSuggestion(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async copilotSuggestion(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getActiveFile();
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
const articleDetails = await ArticleHelper.getFrontMatterByPath(article);
|
||||
if (!articleDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extPath = Extension.getInstance().extensionPath;
|
||||
const panel = PanelProvider.getInstance(extPath);
|
||||
|
||||
const [model] = await lm.selectChatModels({
|
||||
vendor: 'copilot',
|
||||
// family: 'gpt-4'
|
||||
});
|
||||
|
||||
// TODO: Create settings for: copilot.description.message, copilot.family,
|
||||
|
||||
const chars = Settings.get<number>(SETTING_SEO_DESCRIPTION_LENGTH) || 160;
|
||||
const messages = [
|
||||
LanguageModelChatMessage.User(
|
||||
`You are a CMS expert for Front Matter CMS and your task is to assist the user to generate a SEO friendly abstract/description for their article. When the user provides a title and/or content, you should use this information to generate the description.
|
||||
|
||||
IMPORTANT: You are only allowed to respond with a text that should not exceed ${chars} characters in length.`
|
||||
)
|
||||
];
|
||||
|
||||
if (articleDetails && articleDetails.data?.title) {
|
||||
messages.push(LanguageModelChatMessage.User(
|
||||
`The title of the blog post is """${articleDetails.data.title}""".`
|
||||
));
|
||||
}
|
||||
|
||||
if (articleDetails && articleDetails.content) {
|
||||
messages.push(LanguageModelChatMessage.User(
|
||||
`The content of the blog post is: """${articleDetails.content}""".`
|
||||
));
|
||||
}
|
||||
|
||||
let chatResponse: LanguageModelChatResponse | undefined;
|
||||
|
||||
try {
|
||||
chatResponse = await model.sendRequest(messages, {}, new CancellationTokenSource().token);
|
||||
} catch (err) {
|
||||
Logger.error(`DataListener:copilotSuggestion:: ${(err as Error).message}`);
|
||||
panel.getWebview()?.postMessage({
|
||||
command,
|
||||
requestId,
|
||||
error: l10n.t(LocalizationKey.listenersPanelDataListenerAiSuggestTaxonomyNoDataError)
|
||||
} as MessageHandlerData<string>);
|
||||
return;
|
||||
}
|
||||
|
||||
let allFragments = [];
|
||||
for await (const fragment of chatResponse.text) {
|
||||
allFragments.push(fragment);
|
||||
}
|
||||
|
||||
if (allFragments.length > 0) {
|
||||
const description = allFragments.join('');
|
||||
panel.getWebview()?.postMessage({
|
||||
command,
|
||||
requestId,
|
||||
payload: description.trim()
|
||||
} as MessageHandlerData<string>);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1396,6 +1396,10 @@ export enum LocalizationKey {
|
||||
* Use Front Matter AI to suggest {0}
|
||||
*/
|
||||
panelFieldsTextFieldAiMessage = 'panel.fields.textField.ai.message',
|
||||
/**
|
||||
* Use Copilot to suggest {0}
|
||||
*/
|
||||
panelFieldsTextFieldCopilotMessage = 'panel.fields.textField.copilot.message',
|
||||
/**
|
||||
* Generating suggestion...
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface PanelSettings {
|
||||
fieldGroups: FieldGroup[] | undefined;
|
||||
commaSeparatedFields: string[];
|
||||
aiEnabled: boolean;
|
||||
copilotEnabled: boolean;
|
||||
contentFolders: ContentFolder[];
|
||||
websiteUrl: string;
|
||||
disabledActions: PanelAction[];
|
||||
|
||||
@@ -42,6 +42,7 @@ export enum CommandToCode {
|
||||
stopServer = 'stop-server',
|
||||
aiSuggestTaxonomy = 'ai-suggest-taxonomy',
|
||||
aiSuggestDescription = 'ai-suggest-description',
|
||||
copilotDescription = 'copilot-suggest-description',
|
||||
searchByType = 'search-by-type',
|
||||
processMediaData = 'process-media-data',
|
||||
isServerStarted = 'is-server-started'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { RequiredAsterix } from './RequiredAsterix';
|
||||
import { VSCodeLabel } from '../VSCode';
|
||||
|
||||
export interface IFieldTitleProps {
|
||||
label: string | JSX.Element;
|
||||
@@ -23,16 +22,14 @@ export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<VSCodeLabel>
|
||||
<div className={`metadata_field__label ${className || ''}`}>
|
||||
<div>
|
||||
{Icon}
|
||||
<span style={{ lineHeight: '16px' }}>{label}</span>
|
||||
<RequiredAsterix required={required} />
|
||||
</div>
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<label className={`metadata_field__label text-base text-[var(--vscode-foreground)] py-2 ${className || ''}`}>
|
||||
{Icon}
|
||||
<span style={{ lineHeight: '16px' }}>{label}</span>
|
||||
<RequiredAsterix required={required} />
|
||||
</label>
|
||||
|
||||
{actionElement}
|
||||
</div>
|
||||
</VSCodeLabel>
|
||||
{actionElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CommandToCode } from '../../CommandToCode';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
import { CopilotIcon } from '../Icons';
|
||||
|
||||
const DEBOUNCE_TIME = 300;
|
||||
|
||||
@@ -91,9 +92,9 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
}
|
||||
}, [showRequiredState, isValid]);
|
||||
|
||||
const suggestDescription = () => {
|
||||
const suggestDescription = (type: "ai" | "copilot") => {
|
||||
setLoading(true);
|
||||
messageHandler.request<string>(CommandToCode.aiSuggestDescription).then((suggestion) => {
|
||||
messageHandler.request<string>(type === "copilot" ? CommandToCode.copilotDescription : CommandToCode.aiSuggestDescription).then((suggestion) => {
|
||||
setLoading(false);
|
||||
|
||||
if (suggestion) {
|
||||
@@ -106,19 +107,38 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
};
|
||||
|
||||
const actionElement = useMemo(() => {
|
||||
if (!settings?.aiEnabled || settings.seo.descriptionField !== name) {
|
||||
if (settings.seo.descriptionField !== name) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className='metadata_field__title__action'
|
||||
title={l10n.t(LocalizationKey.panelFieldsTextFieldAiMessage, label?.toLowerCase())}
|
||||
type='button'
|
||||
onClick={() => suggestDescription()}
|
||||
disabled={loading}>
|
||||
<SparklesIcon />
|
||||
</button>
|
||||
<div className='flex gap-4'>
|
||||
{
|
||||
settings?.aiEnabled && (
|
||||
<button
|
||||
className='metadata_field__title__action inline-block text-[var(--vscode-editor-foreground)] disabled:opacity-50'
|
||||
title={l10n.t(LocalizationKey.panelFieldsTextFieldAiMessage, label?.toLowerCase())}
|
||||
type='button'
|
||||
onClick={() => suggestDescription("ai")}
|
||||
disabled={loading}>
|
||||
<SparklesIcon />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
settings?.copilotEnabled && (
|
||||
<button
|
||||
className='metadata_field__title__action inline-block text-[var(--vscode-editor-foreground)] disabled:opacity-50'
|
||||
title={l10n.t(LocalizationKey.panelFieldsTextFieldCopilotMessage, label?.toLowerCase())}
|
||||
type='button'
|
||||
onClick={() => suggestDescription("copilot")}
|
||||
disabled={loading}>
|
||||
<CopilotIcon />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, [settings?.aiEnabled, name]);
|
||||
|
||||
@@ -145,7 +165,11 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
<FieldTitle label={label} actionElement={actionElement} icon={<PencilIcon />} required={required} />
|
||||
<FieldTitle
|
||||
label={label}
|
||||
actionElement={actionElement}
|
||||
icon={<PencilIcon />}
|
||||
required={required} />
|
||||
|
||||
{wysiwyg ? (
|
||||
<React.Suspense fallback={<div>{l10n.t(LocalizationKey.panelFieldsTextFieldLoading)}</div>}>
|
||||
|
||||
11
src/panelWebView/components/Icons/CopilotIcon.tsx
Normal file
11
src/panelWebView/components/Icons/CopilotIcon.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ICopilotIconProps { }
|
||||
|
||||
export const CopilotIcon: React.FunctionComponent<ICopilotIconProps> = (props: React.PropsWithChildren<ICopilotIconProps>) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 208">
|
||||
<path fill='currentcolor' d="M205.28 31.36c14.096 14.88 20.016 35.2 22.512 63.68c6.626 0 12.805 1.47 16.976 7.152l7.792 10.56A17.55 17.55 0 0 1 256 123.2v28.688c-.008 3.704-1.843 7.315-4.832 9.504C215.885 187.222 172.35 208 128 208c-49.066 0-98.19-28.273-123.168-46.608c-2.989-2.189-4.825-5.8-4.832-9.504V123.2c0-3.776 1.2-7.424 3.424-10.464l7.792-10.544c4.173-5.657 10.38-7.152 16.992-7.152c2.496-28.48 8.4-48.8 22.512-63.68C77.331 3.165 112.567.06 127.552 0H128c14.72 0 50.4 2.88 77.28 31.36m-77.264 47.376c-3.04 0-6.544.176-10.272.544c-1.312 4.896-3.248 9.312-6.08 12.128c-11.2 11.2-24.704 12.928-31.936 12.928c-6.802 0-13.927-1.42-19.744-5.088c-5.502 1.808-10.786 4.415-11.136 10.912c-.586 12.28-.637 24.55-.688 36.824c-.026 6.16-.05 12.322-.144 18.488c.024 3.579 2.182 6.903 5.44 8.384C79.936 185.92 104.976 192 128.016 192c23.008 0 48.048-6.08 74.512-18.144c3.258-1.48 5.415-4.805 5.44-8.384c.317-18.418.062-36.912-.816-55.312h.016c-.342-6.534-5.648-9.098-11.168-10.912c-5.82 3.652-12.927 5.088-19.728 5.088c-7.232 0-20.72-1.728-31.936-12.928c-2.832-2.816-4.768-7.232-6.08-12.128a106 106 0 0 0-10.24-.544m-26.941 43.93c5.748 0 10.408 4.66 10.408 10.409v19.183c0 5.749-4.66 10.409-10.408 10.409s-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408m53.333 0c5.749 0 10.409 4.66 10.409 10.409v19.183c0 5.749-4.66 10.409-10.409 10.409c-5.748 0-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408M81.44 28.32c-11.2 1.12-20.64 4.8-25.44 9.92c-10.4 11.36-8.16 40.16-2.24 46.24c4.32 4.32 12.48 7.2 21.28 7.2c6.72 0 19.52-1.44 30.08-12.16c4.64-4.48 7.52-15.68 7.2-27.04c-.32-9.12-2.88-16.64-6.72-19.84c-4.16-3.68-13.6-5.28-24.16-4.32m68.96 4.32c-3.84 3.2-6.4 10.72-6.72 19.84c-.32 11.36 2.56 22.56 7.2 27.04c10.56 10.72 23.36 12.16 30.08 12.16c8.8 0 16.96-2.88 21.28-7.2c5.92-6.08 8.16-34.88-2.24-46.24c-4.8-5.12-14.24-8.8-25.44-9.92c-10.56-.96-20 .64-24.16 4.32M128 56c-2.56 0-5.6.16-8.96.48c.32 1.76.48 3.68.64 5.76c0 1.44 0 2.88-.16 4.48c3.2-.32 5.92-.32 8.48-.32s5.28 0 8.48.32c-.16-1.6-.16-3.04-.16-4.48c.16-2.08.32-4 .64-5.76c-3.36-.32-6.4-.48-8.96-.48" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export * from './ArchiveIcon';
|
||||
export * from './BranchIcon';
|
||||
export * from './BugIcon';
|
||||
export * from './CenterIcon';
|
||||
export * from './CopilotIcon';
|
||||
export * from './FileIcon';
|
||||
export * from './FolderOpenedIcon';
|
||||
export * from './FrontMatterIcon';
|
||||
|
||||
@@ -326,31 +326,30 @@ button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
.metadata_field__title__action {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-button-hoverBackground);
|
||||
fill: var(--vscode-button-hoverBackground);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metadata_field__title__action {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
color: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--vscode-button-hoverBackground);
|
||||
fill: var(--vscode-button-hoverBackground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
color: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,15 +363,15 @@ button {
|
||||
}
|
||||
|
||||
.metadata_field__loading {
|
||||
border-radius: 0.25rem;
|
||||
backdrop-filter: blur(15px);
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: calc(100% + 2.5em);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
top: 30px;
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
top: 32px;
|
||||
left: -1.25rem;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
8
src/services/Copilot.ts
Normal file
8
src/services/Copilot.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { extensions } from "vscode";
|
||||
|
||||
export class Copilot {
|
||||
public static async isInstalled(): Promise<boolean> {
|
||||
const copilotExt = extensions.getExtension(`GitHub.copilot`);
|
||||
return !!copilotExt;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user