Skip to content

Commit

Permalink
(Experimental) Add new command to decorate contents (#14)
Browse files Browse the repository at this point in the history
* feat: move context menu to submenu of Slidaiv

* feat: add new command to decorate contents

* refactor: move generate task to tasks.ts

* fix a bit

* chore: change order of context menu
  • Loading branch information
kaakaa authored Jul 6, 2024
1 parent 0c8e717 commit 14f385f
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 78 deletions.
38 changes: 29 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,37 @@
{
"command": "slidaiv.generateContents",
"title": "Generate Slidev contents"
},
{
"command": "slidaiv.decorateContents",
"title": "Decorate contents (Experimental)"
}
],
"submenus": [
{
"id": "slidaiv.submenu",
"label": "Slidaiv"
}
],
"menus": {
"editor/context": [
{
"submenu": "slidaiv.submenu",
"when": "editorLangId == markdown",
"group": "1_modification@99"
}
],
"slidaiv.submenu": [
{
"command": "slidaiv.generateContents",
"group": "slidaiv@1"
},
{
"command": "slidaiv.decorateContents",
"group": "slidaiv@2"
}
]
},
"configuration": {
"title": "Slidaiv",
"properties": {
Expand All @@ -43,15 +72,6 @@
"description": "Model name"
}
}
},
"menus": {
"editor/context": [
{
"command": "slidaiv.generateContents",
"when": "editorLangId == markdown",
"group": "1_modification@99"
}
]
}
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/client/llmClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
interface LLMClient {
generatePageContents(prompt: string, model: string, locale: string | null): Promise<string | null>;
generatePageContents(prompt: string, locale: string | null): Promise<string | null>;
decorateContents(prompt: string): Promise<string | null>;
}
25 changes: 21 additions & 4 deletions src/client/openai.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import OpenAI from 'openai';
import { getGenerateContentsPrompt } from './prompts';
import { getDecorateContentsPrompt, getGenerateContentsPrompt } from './prompts';
import { getLocaleName } from '../utils';

export class Client implements LLMClient {
private client: OpenAI;
private llmModel: string;
private defaultLocale: string;

constructor(apiKey:string, baseURL:string|null, locale: string) {
constructor(apiKey:string, baseURL:string|null, llmModel: string, locale: string) {
baseURL = baseURL || 'https://api.openai.com/v1';
this.client = new OpenAI({apiKey, baseURL});
this.llmModel = llmModel;
this.defaultLocale = locale;
}

async generatePageContents(prompt: string, model:string, locale: string | null): Promise<string | null> {
async generatePageContents(prompt: string, locale: string | null): Promise<string | null> {
const loc = getLocaleName(locale || this.defaultLocale);
const sysPrompt = getGenerateContentsPrompt(loc);
const resp = await this.client.chat.completions.create({
model: model,
model: this.llmModel,
messages: [{
"content": prompt,
"role": "user",
Expand All @@ -27,5 +29,20 @@ export class Client implements LLMClient {
});
return resp.choices[0].message.content;
}

async decorateContents(prompt: string): Promise<string | null> {
const sysPrompt = getDecorateContentsPrompt();
const resp = await this.client.chat.completions.create({
model: this.llmModel,
messages: [{
"content": prompt,
"role": "user",
}, {
"content": sysPrompt,
"role": "system"
}],
});
return resp.choices[0].message.content;
}
}

17 changes: 16 additions & 1 deletion src/client/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,19 @@ export function getGenerateContentsPrompt(locale: string) {
Now, please provide instructions for the slide content.
`;
}
}

export function getDecorateContentsPrompt() {
return `
You are a Slidev Markdown decorator using Tailwind CSS and UnoCSS. Your task is to process the input Markdown and return ONLY the decorated input text. Follow these rules:
1. The input will be a portion of Slidev Markdown text.
2. Must preserve all original text, Markdown structure, and line breaks exactly.
3. Should add Tailwind CSS or UnoCSS classes to enhance visual appeal.
4. Must not add any new text, explanations, or comments.
5. Must not introduce any new line breaks, including those that might be caused by HTML tags like <p> or <h1>.
6. Must output only the decorated Markdown, nothing else such as explanations of the decoration.
Your entire response must be valid Slidev Markdown, identical to the input except for added CSS classes.
`;
}
99 changes: 38 additions & 61 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,47 @@ import * as vscode from 'vscode';

import { ExtensionID } from './constants';
import { Client } from './client/openai';
import { SlidevPage } from './model/slidev';
import { Logger } from './logger';
import { getTaskDecorateContent, getTaskGenerateContents } from './tasks';

export function activate(context: vscode.ExtensionContext) {
const apiKey:string = vscode.workspace.getConfiguration(ExtensionID).get('apiKey') || '';
const baseUrl:string|null = vscode.workspace.getConfiguration(ExtensionID).get('baseUrl') || null;
const client = new Client(apiKey, baseUrl, vscode.env.language);

let disposable = vscode.commands.registerCommand('slidaiv.generateContents', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
// TODO: make this configurable
const debugMode = true;
const logger = new Logger(vscode.window.createOutputChannel('Slidaiv'), debugMode);
logger.info('Slidaiv is now active');

const apiKey: string = vscode.workspace.getConfiguration(ExtensionID).get('apiKey') || '';
const baseUrl: string | null = vscode.workspace.getConfiguration(ExtensionID).get('baseUrl') || null;
const llmModel: string = vscode.workspace.getConfiguration(ExtensionID).get('model') || '';
const client = new Client(apiKey, baseUrl, llmModel, vscode.env.language);
logger.info(`#{baseUrl: ${baseUrl}, model: ${llmModel}, locale: ${vscode.env.language}`);


context.subscriptions.push(vscode.commands.registerCommand('slidaiv.generateContents', async () => {
try {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Generating Slidev contents',
cancellable: false
}, getTaskGenerateContents(client, logger));
} catch (e: any) {
vscode.window.showErrorMessage(`failed to generate content: ${e.message}`);
logger.error(`failed to generate content: ${e.message}`);
}

await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Generating Slidev contents',
cancellable: false
}, async (progress) => {
progress.report({ increment: 0, message: 'Parsing Slidev contents'});
const position = editor.selection.active;

let slidevPage: SlidevPage;
let page: string;
// parse text and generate contents from LLM
try {
slidevPage = await SlidevPage.init(
editor.document.getText(),
editor.document.fileName,
position.line
);
progress.report({ increment: 10, message: 'Generating Slidev contents'});
const llmModel: string = vscode.workspace.getConfiguration(ExtensionID).get('model') || '';
page = await slidevPage.rewriteByLLM(client, llmModel);
} catch (e: any) {
vscode.window.showErrorMessage(`failed to generate Slidev content: ${e.message}`);
progress.report({ increment: 100 });
return;
}

progress.report({ increment: 40, message: 'Replace the slide contents'});

// apply the generated contents to the editor
try {
const range = new vscode.Range(slidevPage.start, 0, slidevPage.end, 0);
const edit = new vscode.WorkspaceEdit();
edit.replace(editor.document.uri, range , page);
const isEdited = await vscode.workspace.applyEdit(edit);
if (!isEdited) {
vscode.window.showErrorMessage('Failed to replace the slide contents');
progress.report({ increment: 10 });
return;
}
progress.report({ increment: 50 });
} catch (e: any) {
vscode.window.showErrorMessage(`failed to apply content: ${e.message}`);
progress.report({ increment: 100 });
return;
}
});
});

context.subscriptions.push(disposable);
}));
context.subscriptions.push(vscode.commands.registerCommand('slidaiv.decorateContents', async () => {
try {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'Decorating Slidev contents',
cancellable: false
}, getTaskDecorateContent(client, logger));
} catch (e: any) {
vscode.window.showErrorMessage(`failed to decorate content: ${e.message}`);
logger.error(`failed to decorate content: ${e.message}`);
}
}));
}

// This method is called when your extension is deactivated
export function deactivate() {}
export function deactivate() { }
23 changes: 23 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type * as vscode from 'vscode';

export class Logger {
private readonly out: vscode.OutputChannel;
private isDebug: boolean;
constructor(out: vscode.OutputChannel, debug: boolean) {
this.out = out;
this.isDebug = debug;
}

error(message: string) {
this.out.appendLine(`[ERROR] ${message}`);
}

info(message: string) {
this.out.appendLine(`[INFO] ${message}`);
}

debug(message: string) {
if (!this.isDebug) return;

Check warning on line 20 in src/logger.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
this.out.appendLine(`[DEBUG] ${message}`);
}
}
4 changes: 2 additions & 2 deletions src/model/slidev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export class SlidevPage {
return new SlidevPage(slide);
}

async rewriteByLLM(client: LLMClient, model: string) {
async rewriteByLLM(client: LLMClient) {
const prompt = this.prompts.map((prompt: string) => `- ${prompt}`).join('\n');
const content = await client.generatePageContents(prompt, model, this.locale);
const content = await client.generatePageContents(prompt, this.locale);
return `${obj2frontmatter(this.frontmatter)}\n\n${content}\n\n`;
}

Expand Down
74 changes: 74 additions & 0 deletions src/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as vscode from 'vscode';

import { Client } from "./client/openai";
import { Logger } from "./logger";
import { SlidevPage } from './model/slidev';

export const getTaskGenerateContents = (client: Client, logger: Logger) => {
return async (progress: vscode.Progress<any>) => {
logger.info('Generating contents');
progress.report({ increment: 0, message: 'Parsing Slidev contents' });
const editor = vscode.window.activeTextEditor;
if (!editor) {
throw new Error('No active editor');
}
const position = editor.selection.active;
const slidevPage = await SlidevPage.init(
editor.document.getText(),
editor.document.fileName,
position.line
);

progress.report({ increment: 10, message: 'Generating Slidev contents' });
const page = await slidevPage.rewriteByLLM(client);

progress.report({ increment: 80, message: 'Write the generated slide contents' });
const range = new vscode.Range(slidevPage.start, 0, slidevPage.end, 0);
const edit = new vscode.WorkspaceEdit();
edit.replace(editor.document.uri, range, page);
const isEdited = await vscode.workspace.applyEdit(edit);
if (!isEdited) {
throw new Error('Failed to write the generated slide contents');
}

progress.report({ increment: 10, message: 'Done' });
}

Check warning on line 35 in src/tasks.ts

View workflow job for this annotation

GitHub Actions / build

Missing semicolon
}

Check warning on line 36 in src/tasks.ts

View workflow job for this annotation

GitHub Actions / build

Missing semicolon

export const getTaskDecorateContent = (client: Client, logger: Logger) => {
return async (progress: vscode.Progress<any>) => {
logger.info('Decorating contents');
progress.report({ increment: 0, message: 'Get text to decorate' });
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showErrorMessage('No active editor');
return;
}
const selection = editor.selection;
if (!selection || selection.isEmpty) {
vscode.window.showErrorMessage('No selection');
return;
}
const highlighted = editor.document.getText(selection);
logger.debug(`selection: \n${highlighted}`);

progress.report({ increment: 10, message: 'Calling LLM...' });
logger.info('Call LLM to decorate the contents');
const decorated = await client.decorateContents(highlighted);
logger.debug(`decorated: \n${decorated}`);
if (!decorated) {
throw new Error('Failed to decorate the contents');
}

progress.report({ increment: 80, message: 'Write the decorated text' });
logger.info('Write the slide contents');
const edit = new vscode.WorkspaceEdit();
edit.replace(editor.document.uri, selection, decorated);
const isEdited = await vscode.workspace.applyEdit(edit);
if (!isEdited) {
throw new Error('Failed to replace the slide contents');
}

progress.report({ increment: 10, message: 'Done' });
}

Check warning on line 73 in src/tasks.ts

View workflow job for this annotation

GitHub Actions / build

Missing semicolon
}

Check warning on line 74 in src/tasks.ts

View workflow job for this annotation

GitHub Actions / build

Missing semicolon

0 comments on commit 14f385f

Please sign in to comment.