manibatra_obsidian-text2ank.../main.ts

307 lines
7.5 KiB
TypeScript
Raw Permalink Normal View History

2023-03-27 11:37:55 +00:00
import {
App,
Plugin,
PluginSettingTab,
Setting,
Notice,
requestUrl,
} from "obsidian";
2023-03-21 13:03:59 +00:00
import { Configuration, OpenAIApi } from "openai";
interface FlashcardGeneratorSettings {
modelName: string;
2023-03-24 11:33:50 +00:00
prompt: string;
2023-03-21 13:03:59 +00:00
apiKey: string;
deckName: string;
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
const DEFAULT_SETTINGS: FlashcardGeneratorSettings = {
apiKey: "",
deckName: "Generated Flashcards",
modelName: "gpt-4",
2023-03-24 11:33:50 +00:00
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 '======='",
2023-03-21 13:03:59 +00:00
};
2023-03-21 09:48:36 +00:00
2023-03-21 13:03:59 +00:00
export default class FlashcardGeneratorPlugin extends Plugin {
settings: FlashcardGeneratorSettings;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
openai: any;
2023-03-21 09:48:36 +00:00
async onload() {
2023-03-21 13:03:59 +00:00
console.log("loading Flashcard Generator plugin");
2023-03-21 09:48:36 +00:00
await this.loadSettings();
2023-03-21 13:03:59 +00:00
this.addCommand({
id: "generate-flashcards",
2023-03-24 11:33:50 +00:00
name: "Generate flashcards from current file",
2023-03-21 13:03:59 +00:00
callback: () => this.generateFlashcardsFromCurrentFile(),
2023-03-21 09:48:36 +00:00
});
2023-03-21 13:03:59 +00:00
this.addSettingTab(new FlashcardGeneratorSettingTab(this.app, this));
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
const configuration = new Configuration({
apiKey: this.settings.apiKey,
2023-03-21 09:48:36 +00:00
});
2023-03-21 13:03:59 +00:00
this.openai = new OpenAIApi(configuration);
}
async saveSettings() {
await this.saveData(this.settings);
}
2023-03-24 11:33:50 +00:00
async generateFlashcardsFromCurrentFile() {
2023-03-21 13:03:59 +00:00
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
2023-03-27 11:37:29 +00:00
if (noteFile === null) return; // Nothing Open
2023-03-21 13:03:59 +00:00
const text = await this.app.vault.read(noteFile);
if (!text) {
new Notice(`Couldn't retrieve any text`);
return;
}
2023-03-24 11:33:50 +00:00
// Extract the deck name from the note text
const deckNameRegex = /^Deck:\s*(.+?)$/m;
const match = text.match(deckNameRegex);
const deckName = match ? match[1] : this.settings.deckName;
// Remove the "Deck: ..." line from the text
const cleanedText = text.replace(deckNameRegex, "").trim();
2023-03-21 13:03:59 +00:00
try {
2023-03-24 11:33:50 +00:00
// Display a waiting sign
const waitingNotice = new Notice(
"Generating flashcards, please wait...",
120000
);
2023-03-21 13:03:59 +00:00
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",
2023-03-24 11:33:50 +00:00
content: this.settings.prompt,
2023-03-21 13:03:59 +00:00
},
{
role: "user",
content:
2023-03-24 11:33:50 +00:00
"Create flashcards from the following text:" +
cleanedText,
2023-03-21 13:03:59 +00:00
},
],
model: this.settings.modelName,
temperature: 0.2,
presence_penalty: -0.2,
});
2023-03-24 11:33:50 +00:00
waitingNotice.hide();
2023-03-21 13:03:59 +00:00
const generatedFlashcards =
response.data.choices[0].message.content;
let deckId = await this.getDeckId(deckName);
if (!deckId) {
new Notice(
2023-03-24 11:33:50 +00:00
`Could not find deck with name '${deckName}'. Creating it`
2023-03-21 13:03:59 +00:00
);
await this.createDeck(deckName);
deckId = await this.getDeckId(deckName);
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
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();
2023-03-21 09:48:36 +00:00
2023-03-24 11:33:50 +00:00
await this.addCardToDeck(
{
front,
back,
},
deckName
);
2023-03-21 13:03:59 +00:00
}
}
2023-03-21 09:48:36 +00:00
2023-03-21 13:03:59 +00:00
new Notice("Flashcards generated and added to Anki!");
} catch (error) {
console.error(error);
new Notice("An error occurred while generating flashcards");
}
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
async getDeckId(name: string) {
try {
const decks = await this.invokeAnkiConnect("deckNamesAndIds");
2023-03-27 12:03:57 +00:00
if (decks[name]) {
return decks[name];
2023-03-21 13:03:59 +00:00
}
} catch (error) {
console.error(error);
}
2023-03-21 09:48:36 +00:00
2023-03-21 13:03:59 +00:00
return null;
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
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");
}
2023-03-21 09:48:36 +00:00
}
2023-03-24 11:33:50 +00:00
async addCardToDeck(
note: { front: string; back: string },
deckName: string
) {
2023-03-21 13:03:59 +00:00
const { front, back } = note;
2023-03-21 09:48:36 +00:00
2023-03-21 13:03:59 +00:00
try {
await this.invokeAnkiConnect("addNote", {
note: {
2023-03-24 11:33:50 +00:00
deckName: deckName,
2023-03-21 13:03:59 +00:00
modelName: "Basic",
fields: {
Front: front,
Back: back,
},
options: {
allowDuplicate: false,
},
tags: ["generated"],
},
});
} catch (error) {
console.error(error);
2023-03-27 12:03:29 +00:00
new Notice(error);
2023-03-21 13:03:59 +00:00
}
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
async invokeAnkiConnect(action: string, params?: any) {
2023-03-27 11:37:55 +00:00
const response = await requestUrl({
url: "http://localhost:8765",
2023-03-21 13:03:59 +00:00
method: "POST",
body: JSON.stringify({
action,
params,
version: 6,
}),
});
2023-03-27 12:03:29 +00:00
const parsedResponse = await response.json;
2023-03-21 13:03:59 +00:00
if (parsedResponse.hasOwnProperty("error")) {
2023-03-27 12:03:29 +00:00
if (parsedResponse.error !== null) {
throw new Error(parsedResponse.error);
}
2023-03-21 13:03:59 +00:00
}
return parsedResponse.result;
2023-03-21 09:48:36 +00:00
}
2023-03-21 13:03:59 +00:00
onunload() {
console.log("unloading Flashcard Generator plugin");
2023-03-21 09:48:36 +00:00
}
}
2023-03-21 13:03:59 +00:00
class FlashcardGeneratorSettingTab extends PluginSettingTab {
plugin: FlashcardGeneratorPlugin;
2023-03-21 09:48:36 +00:00
2023-03-21 13:03:59 +00:00
constructor(app: App, plugin: FlashcardGeneratorPlugin) {
2023-03-21 09:48:36 +00:00
super(app, plugin);
this.plugin = plugin;
}
display(): void {
2023-03-21 13:03:59 +00:00
const { containerEl } = this;
2023-03-21 09:48:36 +00:00
containerEl.empty();
2023-03-21 13:03:59 +00:00
containerEl.createEl("h2", {
text: "Flashcard Generator plugin Settings",
});
new Setting(containerEl)
.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();
})
);
2023-03-21 09:48:36 +00:00
new Setting(containerEl)
2023-03-21 13:03:59 +00:00
.setName("OpenAI GPT Model Name")
.setDesc("Name of the OpenAI GPT model to use")
.addDropdown((dropdown) =>
dropdown
.addOption("gpt-4", "gpt-4")
2023-03-24 11:33:50 +00:00
.addOption("gpt-3.5-turbo", "gpt-3.5-turbo")
.addOption("text-davinci-003", "text-davinci-003")
2023-03-21 13:03:59 +00:00
.setValue(this.plugin.settings.modelName)
.onChange(async (value) => {
this.plugin.settings.modelName = value;
await this.plugin.saveSettings();
})
);
2023-03-24 11:33:50 +00:00
new Setting(containerEl)
.setName("OpenAI GPT Model Prompt")
.setDesc("User Prompt to use for the OpenAI GPT model")
.addText((text) =>
text
.setPlaceholder(
"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 '======='"
)
.setValue(this.plugin.settings.prompt)
.onChange(async (value) => {
this.plugin.settings.prompt = value;
await this.plugin.saveSettings();
})
);
2023-03-21 09:48:36 +00:00
}
}