Skip to content

Commit

Permalink
Implement flashcards generation with user input
Browse files Browse the repository at this point in the history
- Refactoring: move FlashcardsSettings and FlashcardsSettingsTab into a
  separate file
- Implement InputModal which lets the user specify custom settings on a
  per-command basis
crybot committed May 30, 2024

Verified

This commit was signed with the committer’s verified signature.
jojomatik jojomatik
1 parent d02312f commit 42eb0a7
Showing 4 changed files with 397 additions and 261 deletions.
235 changes: 143 additions & 92 deletions main.js

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { App, Modal, Setting } from "obsidian"
import { FlashcardsSettings } from "./settings"


export class InputModal extends Modal {
configuration: FlashcardsSettings;
multiline: boolean;
onSubmit: (configuration: FlashcardsSettings, multiline: boolean) => void;

constructor(app: App, plugin: FlashcardsLLMPlugin, onSubmit: (configuration: FlashcardsSettings, multiline: boolean) => void) {
super(app);
this.plugin = plugin;
this.onSubmit = onSubmit;
this.configuration = { ...this.plugin.settings };
}

onOpen() {
let { contentEl } = this;
contentEl.createEl("h1", { text: "Prompt configuration" });

new Setting(contentEl)
.setName("Number of flashcards to generate")
.addText((text) =>
text
.setValue(this.configuration.flashcardsCount.toString())
.onChange((value) => {
this.configuration.flashcardsCount = Number(value)
// TODO: check input
})
);

new Setting(contentEl)
.setName("Additional prompt")
.addText((text) =>
text
.setValue(this.configuration.additionalPrompt)
.onChange((value) => {
this.configuration.additionalPrompt = value
})
);

new Setting(contentEl)
.setName("Multiline")
.addToggle((on) =>
on
.setValue(false)
.onChange(async (on) => {
this.multiline = on
})
);

new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText("Submit")
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.configuration, this.multiline);
})
);

}

onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
196 changes: 27 additions & 169 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { App, Editor, EditorPosition, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { generateFlashcards } from "./flashcards";
import { availableChatModels, availableCompletionModels } from "./models";
import { InputModal } from "./components"
import { FlashcardsSettings, FlashcardsSettingsTab } from "./settings"

// TODO:
// - Status bar
// - Modal input for custom on the fly settings (prompts/flashcards count, etc.)
// - Enforce newline separation (stream post processing)
// - Always append flashcards at the end of the file (ch:0, line: last)
// - Disable user input while generating
// - Custom tag for flashcards blocks
// - Insert an optional header before flashcards

interface FlashcardsSettings {
apiKey: string;
model: string;
inlineSeparator: string;
multilineSeparator: string;
flashcardsCount: number;
additionalPrompt: string;
maxTokens: number;
streaming: boolean;
hideInPreview: boolean;
}

const DEFAULT_SETTINGS: FlashcardsSettings = {
apiKey: "",
model: "gpt-4o",
@@ -45,15 +34,27 @@ export default class FlashcardsLLMPlugin extends Plugin {
id: "generate-inline-flashcards",
name: "Generate Inline Flashcards",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.onGenerateFlashcards(editor, view, false);
this.onGenerateFlashcards(editor, view, this.settings, false);
},
});

this.addCommand({
id: "generate-long-flashcards",
name: "Generate Multiline Flashcards",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.onGenerateFlashcards(editor, view, true);
this.onGenerateFlashcards(editor, view, this.settings, true);
},
});

this.addCommand({
id: "generate-flashcards-interactive",
name: "Generate flashcards with new settings",
editorCallback: (editor: Editor, view: MarkdownView) => {

new InputModal(this.app, this, (configuration: FlashcardsSettings, multiline: boolean) => {
this.onGenerateFlashcards(editor, view, configuration, multiline);
}).open();

},
});

@@ -78,33 +79,29 @@ export default class FlashcardsLLMPlugin extends Plugin {
this.addSettingTab(new FlashcardsSettingsTab(this.app, this));
}

async onGenerateFlashcards(editor: Editor, view: MarkdownView, multiline: boolean = false) {
console.log(editor)
console.log(view)

console.log(view.previewMode)
const apiKey = this.settings.apiKey;
async onGenerateFlashcards(editor: Editor, view: MarkdownView, configuration: FlashcardsSettings, multiline: boolean = false) {
const apiKey = configuration.apiKey;
if (!apiKey) {
new Notice("API key is not set in plugin settings");
return;
}
const model = this.settings.model;
const model = configuration.model;
if (!model) {
new Notice("Please select a model to use in the plugin settings");
return;
}

const sep = multiline ? this.settings.multilineSeparator : this.settings.inlineSeparator
const sep = multiline ? configuration.multilineSeparator : configuration.inlineSeparator

let flashcardsCount = Math.trunc(this.settings.flashcardsCount)
let flashcardsCount = Math.trunc(configuration.flashcardsCount)
if (!Number.isFinite(flashcardsCount) || flashcardsCount <= 0) {
new Notice("Please provide a correct number of flashcards to generate. Defaulting to 3")
flashcardsCount = 3
}

const additionalPrompt = this.settings.additionalPrompt
let additionalPrompt = configuration.additionalPrompt

let maxTokens = Math.trunc(this.settings.maxTokens)
let maxTokens = Math.trunc(configuration.maxTokens)
if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
new Notice("Please provide a correct number of maximum tokens to generate. Defaulting to 300")
maxTokens = 300
@@ -121,8 +118,11 @@ export default class FlashcardsLLMPlugin extends Plugin {
const hasTag = tagRegex.test(wholeText);


const streaming = this.settings.streaming
const streaming = configuration.streaming
new Notice("Generating flashcards...");

await flashcardsCount

try {
const generatedFlashcards = await generateFlashcards(
currentText,
@@ -181,145 +181,3 @@ export default class FlashcardsLLMPlugin extends Plugin {
}
}

class FlashcardsSettingsTab extends PluginSettingTab {
plugin: FlashcardsLLMPlugin;

constructor(app: App, plugin: FlashcardsLLMPlugin) {
super(app, plugin);
this.plugin = plugin;
}

display(): void {
const { containerEl } = this;

containerEl.empty();

containerEl.createEl("h3", {text: "Model settings"})

new Setting(containerEl)
.setName("OpenAI API key")
.setDesc("Enter your OpenAI API key")
.addText((text) =>
text
.setPlaceholder("API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Model")
.setDesc("Which language model to use")
.addDropdown((dropdown) =>
dropdown
.addOptions(Object.fromEntries(availableCompletionModels().map(k => [k, k])))
.addOptions(Object.fromEntries(availableChatModels().map(k => [k, k])))
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
})
);

containerEl.createEl("h3", {text: "Preferences"})

new Setting(containerEl)
.setName("Separator for inline flashcards")
.setDesc("Note that after changing this you have to manually edit any flashcards you already have")
.addText((text) =>
text
.setPlaceholder("::")
.setValue(this.plugin.settings.inlineSeparator)
.onChange(async (value) => {
this.plugin.settings.inlineSeparator = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Separator for multi-line flashcards")
.setDesc("Note that after changing this you have to manually edit any flashcards you already have")
.addText((text) =>
text
.setPlaceholder("?")
.setValue(this.plugin.settings.multilineSeparator)
.onChange(async (value) => {
this.plugin.settings.multilineSeparator = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Number of flashcards to generate")
.setDesc("Set this to the total number of flashcards the model should "+
"generate each time a new `Generate Flashcards` command is issued")
.addText((text) =>
text
.setPlaceholder("3")
.setValue(this.plugin.settings.flashcardsCount.toString())
.onChange(async (value) => {
this.plugin.settings.flashcardsCount = Number(value);
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Additional prompt")
.setDesc("Provide additional instructions to the language model")
.addText((text) =>
text
.setPlaceholder("Additional instructions")
.setValue(this.plugin.settings.additionalPrompt)
.onChange(async (value) => {
this.plugin.settings.additionalPrompt = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Maximum output tokens")
.setDesc("Set this to the total number of tokens the model can generate")
.addText((text) =>
text
.setPlaceholder("300")
.setValue(this.plugin.settings.maxTokens.toString())
.onChange(async (value) => {
this.plugin.settings.maxTokens = Number(value);
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Streaming")
.setDesc("Enable/Disable streaming text completion")
.addToggle((on) =>
on
.setValue(this.plugin.settings.streaming)
.onChange(async (on) => {
this.plugin.settings.streaming = on;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Hide flashcards in preview mode")
.setDesc("If enabled, you won't see flashcards when in preview mode, "
+ "but you will still be able to edit them")
.addToggle((on) =>
on
.setValue(this.plugin.settings.hideInPreview)
.onChange(async (on) => {
this.plugin.settings.hideInPreview = on;

await this.plugin.saveSettings();

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
view.previewMode.rerender(true);
}
})
);

}
}
158 changes: 158 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { App, MarkdownView, PluginSettingTab, Setting } from 'obsidian';
import { availableChatModels, availableCompletionModels } from "./models";

export interface FlashcardsSettings {
apiKey: string;
model: string;
inlineSeparator: string;
multilineSeparator: string;
flashcardsCount: number;
additionalPrompt: string;
maxTokens: number;
streaming: boolean;
hideInPreview: boolean;
}


export class FlashcardsSettingsTab extends PluginSettingTab {
plugin: FlashcardsLLMPlugin;

constructor(app: App, plugin: FlashcardsLLMPlugin) {
super(app, plugin);
this.plugin = plugin;
}

display(): void {
const { containerEl } = this;

containerEl.empty();

containerEl.createEl("h3", {text: "Model settings"})

new Setting(containerEl)
.setName("OpenAI API key")
.setDesc("Enter your OpenAI API key")
.addText((text) =>
text
.setPlaceholder("API key")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Model")
.setDesc("Which language model to use")
.addDropdown((dropdown) =>
dropdown
.addOptions(Object.fromEntries(availableCompletionModels().map(k => [k, k])))
.addOptions(Object.fromEntries(availableChatModels().map(k => [k, k])))
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
})
);

containerEl.createEl("h3", {text: "Preferences"})

new Setting(containerEl)
.setName("Separator for inline flashcards")
.setDesc("Note that after changing this you have to manually edit any flashcards you already have")
.addText((text) =>
text
.setPlaceholder("::")
.setValue(this.plugin.settings.inlineSeparator)
.onChange(async (value) => {
this.plugin.settings.inlineSeparator = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Separator for multi-line flashcards")
.setDesc("Note that after changing this you have to manually edit any flashcards you already have")
.addText((text) =>
text
.setPlaceholder("?")
.setValue(this.plugin.settings.multilineSeparator)
.onChange(async (value) => {
this.plugin.settings.multilineSeparator = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Number of flashcards to generate")
.setDesc("Set this to the total number of flashcards the model should "+
"generate each time a new `Generate Flashcards` command is issued")
.addText((text) =>
text
.setPlaceholder("3")
.setValue(this.plugin.settings.flashcardsCount.toString())
.onChange(async (value) => {
this.plugin.settings.flashcardsCount = Number(value);
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Additional prompt")
.setDesc("Provide additional instructions to the language model")
.addText((text) =>
text
.setPlaceholder("Additional instructions")
.setValue(this.plugin.settings.additionalPrompt)
.onChange(async (value) => {
this.plugin.settings.additionalPrompt = value;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Maximum output tokens")
.setDesc("Set this to the total number of tokens the model can generate")
.addText((text) =>
text
.setPlaceholder("300")
.setValue(this.plugin.settings.maxTokens.toString())
.onChange(async (value) => {
this.plugin.settings.maxTokens = Number(value);
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Streaming")
.setDesc("Enable/Disable streaming text completion")
.addToggle((on) =>
on
.setValue(this.plugin.settings.streaming)
.onChange(async (on) => {
this.plugin.settings.streaming = on;
await this.plugin.saveSettings();
})
);

new Setting(containerEl)
.setName("Hide flashcards in preview mode")
.setDesc("If enabled, you won't see flashcards when in preview mode, "
+ "but you will still be able to edit them")
.addToggle((on) =>
on
.setValue(this.plugin.settings.hideInPreview)
.onChange(async (on) => {
this.plugin.settings.hideInPreview = on;

await this.plugin.saveSettings();

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
view.previewMode.rerender(true);
}
})
);

}
}

0 comments on commit 42eb0a7

Please sign in to comment.