Init plugin

This commit is contained in:
Mani Batra 2023-03-22 00:03:59 +11:00
parent e8ebfe0fa9
commit 8c33e0cccb
No known key found for this signature in database
GPG key ID: FF32E7D7BF60D915
3 changed files with 4084 additions and 97 deletions

323
main.ts
View file

@ -1,137 +1,266 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
import { App, Plugin, PluginSettingTab, Setting, Notice } from "obsidian";
import { Configuration, OpenAIApi } from "openai";
interface FlashcardGeneratorSettings {
modelName: string;
apiKey: string;
deckName: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
const DEFAULT_SETTINGS: FlashcardGeneratorSettings = {
apiKey: "",
deckName: "Generated Flashcards",
modelName: "gpt-4",
};
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
export default class FlashcardGeneratorPlugin extends Plugin {
settings: FlashcardGeneratorSettings;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
openai: any;
async onload() {
console.log("loading Flashcard Generator plugin");
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
id: "generate-flashcards",
name: "Generate flashcards from bullet points in current file",
callback: () => this.generateFlashcardsFromCurrentFile(),
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
this.addSettingTab(new FlashcardGeneratorSettingTab(this.app, this));
// this.registerDomEvent(document, "click", (event: MouseEvent) => {
// console.log("click", event);
// });
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
const configuration = new Configuration({
apiKey: this.settings.apiKey,
});
this.openai = new OpenAIApi(configuration);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
async generateFlashcardsFromCurrentFile(
prompt = "",
deckName: string = this.settings.deckName
) {
if (!this.settings.apiKey) {
new Notice(
"OpenAI API key is required for Flashcard Generator plugin to work"
);
}
const noteFile = this.app.workspace.getActiveFile(); // Currently Open Note
if (!noteFile?.name) return; // Nothing Open
const text = await this.app.vault.read(noteFile);
if (!text) {
new Notice(`Couldn't retrieve any text`);
return;
}
try {
const response = await this.openai.createChatCompletion({
messages: [
{
role: "system",
content:
"You are an AnkiAssistant that will create flashcards to be used in the Anki App.",
},
{
role: "user",
content:
prompt ||
"You are an AnkiAssistant that will create flashcards to be used in the Anki App. You should use HTML to format parts of the output according to Anki format. Provide code examples and anything that assists in recall. Separate the 'Front' and 'Back' of each flashcard with ||. Only use it once in the flashcard. Every flashcard should be separated by '======='",
},
{
role: "user",
content:
"Create flashcards from the following text:" + text,
},
],
model: this.settings.modelName,
temperature: 0.2,
presence_penalty: -0.2,
});
const generatedFlashcards =
response.data.choices[0].message.content;
let deckId = await this.getDeckId(deckName);
if (!deckId) {
new Notice(
`Could not find deck with name '${this.settings.deckName}'. Creating it`
);
await this.createDeck(deckName);
deckId = await this.getDeckId(deckName);
}
for (const flashcard of generatedFlashcards
.trim()
.split("=======")) {
if (flashcard.length > 0) {
let [front, back] = flashcard.split("||");
front = front.replace("Front:", "").trim();
back = back.replace("Back:", "").trim();
await this.addCardToDeck(deckId, {
front,
back,
});
}
}
new Notice("Flashcards generated and added to Anki!");
} catch (error) {
console.error(error);
new Notice("An error occurred while generating flashcards");
}
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
async getDeckId(name: string) {
try {
const decks = await this.invokeAnkiConnect("deckNamesAndIds");
for (const deck of decks) {
if (deck.name === name) {
return deck.id;
}
}
} catch (error) {
console.error(error);
}
return null;
}
onClose() {
const {contentEl} = this;
contentEl.empty();
async createDeck(deckName: string) {
try {
await this.invokeAnkiConnect("createDeck", {
deck: deckName,
});
} catch (error) {
console.error(error);
new Notice("An error occurred while creating the deck");
}
}
async addCardToDeck(deckId: number, note: { front: string; back: string }) {
const { front, back } = note;
try {
await this.invokeAnkiConnect("addNote", {
note: {
deckName: this.settings.deckName,
modelName: "Basic",
fields: {
Front: front,
Back: back,
},
options: {
allowDuplicate: false,
},
tags: ["generated"],
},
});
} catch (error) {
console.error(error);
new Notice("An error occurred while adding card to deck");
}
}
async invokeAnkiConnect(action: string, params?: any) {
const response = await fetch("http://localhost:8765", {
method: "POST",
body: JSON.stringify({
action,
params,
version: 6,
}),
});
const parsedResponse = await response.json();
if (parsedResponse.hasOwnProperty("error")) {
throw new Error(parsedResponse.error);
}
return parsedResponse.result;
}
onunload() {
console.log("unloading Flashcard Generator plugin");
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
class FlashcardGeneratorSettingTab extends PluginSettingTab {
plugin: FlashcardGeneratorPlugin;
constructor(app: App, plugin: MyPlugin) {
constructor(app: App, plugin: FlashcardGeneratorPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
containerEl.createEl("h2", {
text: "Flashcard Generator plugin Settings",
});
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
console.log('Secret: ' + value);
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
.setName("OpenAI API Key")
.setDesc("API key required for the OpenAI API")
.addText((text) =>
text
.setPlaceholder("Enter API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Anki Deck Name")
.setDesc("Name of the Deck in Anki where flashcards will be added")
.addText((text) =>
text
.setPlaceholder("Enter Deck Name")
.setValue(this.plugin.settings.deckName)
.onChange(async (value) => {
this.plugin.settings.deckName = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("OpenAI GPT Model Name")
.setDesc("Name of the OpenAI GPT model to use")
.addDropdown((dropdown) =>
dropdown
.addOption("gpt-4", "gpt-4")
.addOption("davinci", "davinci")
.setValue(this.plugin.settings.modelName)
.onChange(async (value) => {
this.plugin.settings.modelName = value;
await this.plugin.saveSettings();
})
);
}
}

3855
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -20,5 +20,8 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"openai": "^3.2.1"
}
}