Skip to content

Commit

Permalink
Feature/more accessibility (#9)
Browse files Browse the repository at this point in the history
* fix: make create anki file command not require editor mode

* doc: add next todo

* doc: add readme information

* fix: number checking on export modal

* feat: add buttons for selecting/deselecting all

* feat: add modal for anki deck selection

* fix: remove manual classname settings

* feat: add loading indicator for gpt usage

* doc: readme clarifications, fixes, and status bar
  • Loading branch information
cadrianxyz authored Jun 30, 2023
1 parent cb43a51 commit 007998e
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 142 deletions.
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

Plugin for [Obsidian.md](https://obsidian.md/) that uses OpenAI's GPT LLM to automatically generate Flashcards for Anki.

> The plugin **only works for desktop** (tested on a Mac M1).
> The plugin **only works for desktop**.
The plugin introduces two new commands into obsidian:
The plugin introduces two new "commands" into obsidian:
- _Export Current File to Anki_
- _Export Highlighted Text to Anki_
These commands will **not** be avaialable if you do not have an active `Editable` window open (i.e. you need to have a document open, and it needs to be in `edit` mode).
- _Export Highlighted Text to Anki_ (only available in an active `Editable` window open - i.e. you need to have a document open, and it needs to be in `edit` mode)

The command palette can be accessed on Obsidian through the following hotkey (default): `CMD` + `P`

Expand All @@ -19,44 +18,46 @@ The two new commands look like the following:

![command-palette-new-commands](media/command-palette-new-commands.png)

### Plugin Requirements
## Plugin Requirements

The following are required for the Plugin to work:
- An [OpenAI](https://openai.com/) Account and an [OpenAI API Key](https://platform.openai.com/account/api-keys)
- The [Anki](https://apps.ankiweb.net/) program, installed locally
- [Anki Connect](https://github.com/FooSoft/anki-connect), to expose an Anki API for Obsidian to make calls to

### Plugin Setup
## Plugin Setup

1. Download and install the plugin (Options > Community Plugins)
2. Ensure that you have all the requirements in the [Plugin Requirements](#plugin-requirements)
3. Go to the Plugin Settings (Settings > Community Plugins > Auto Anki) and make sure to set the following fields appropriately:
- Anki Port (by default, this is `8765`)
- Anki Deck Name (by default, this is `Default`)
- OpenAI API Key
- Anki Port (by default, this is `8765`)
- OpenAI API Key
4. Enjoy!

### Feature Details
## Feature Details

- Exporting an Entire File to Anki (Command: _Export Current File to Anki_)
This command allows you to use the contents of the currently-opened file to sends to GPT and generate a list of questions and answers.
#### Exporting an Entire File to Anki (Command: _Export Current File to Anki_)
This command allows you to use the contents of the currently-opened file to send to GPT and generate a list of questions and answers.

![prompt-1](media/prompt-1.png)

Alternatively, you can also specify the _number of alternatives_ to generate for each question. This allows you more variety in the "questions and answers" generated by GPT, as it allows you to choose among a larger number of alternative "questions and answers"
Alternatively, you can also specify the _number of alternatives_ to generate for each question. This allows you more variety in the "questions and answers" generated by GPT and it allows you to choose among a larger number of alternative "questions and answers". Choosing a number of alternatives work best with smaller notes.

![prompt-2](media/prompt-2.png)

From the generated list of "questions and answers", you have the option to pick and choose the ones you want.

After picking and choosing, your selected "questions and answers" automatically imports the chosen questions to Anki, based on the details in your Plugin settings. (Important Note: file needs to be in `edit` mode for the command to be available).
After picking and choosing, your selected "questions and answers" automatically imports the chosen questions to Anki, based on the details in your Plugin settings.

> It may take a while if you are generating a large number of questions, or a large number of alternatives. Future plan is to improve the UI interactions to make this more obvious and seamless.
> It may take a while if you are generating a large number of questions, or a large number of question alternatives.
An indicator will show whether `auto-anki` is currently generating your flash cards. This is shown in the status bar at the bottom of the screen, like below:

- Exporting Highlighted Text to Anki (Command: _Export Highlighted Text to Anki_)
This command is similar to "Exporting an Entire File to Anki", but this allows you to use the currently-highlighted text (instead of the whole file) to send to GPT and generate a list of questions and questions. (Important Note: file also needs to be in `edit` mode for the command to be available).
![status-bar-running](media/status-bar-running.png)

### Motivation
#### Exporting Highlighted Text to Anki (Command: _Export Highlighted Text to Anki_)
This command is similar to "Exporting an Entire File to Anki", but this allows you to use the currently-highlighted text (instead of the whole file) to send to GPT and generate a list of questions and questions. (Important Note: file needs to be in `edit` mode for the command to be available).

## Motivation

With the kajillion things I read and watch on a daily basis, I've recently found myself struggling to retain knowledge of the things I've consumed. Hence, I've found myself trying to find new ways to enhance my self-education. I came upon [Spaced Repetition](https://en.wikipedia.org/wiki/Spaced_repetition), and wanted to try to use [Anki](https://apps.ankiweb.net/) to supplement my daily learnings. Being a long-time user and lover of [Obsidian.md](https://obsidian.md/) as my PKM (Personal Knowledge Management), I wanted to see if there was a way to automate my learning using spaced repetition with my current Obsidian vaults.

Expand Down
Binary file added media/status-bar-running.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 101 additions & 46 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PluginSettingTab,
Setting,
Notice,
setIcon,
} from 'obsidian';

import {
Expand All @@ -19,11 +20,13 @@ import { ExportModal } from './modal';
// electronDecrypt,
// } from './utils/enc';
import { ANKI_CONNECT_DEFAULT_PORT } from './utils/anki';
import { isNumeric } from './utils/utils';
import { isNumeric } from './utils/validations';
import { StatusBarElement } from './utils/cusom-types';

export default class AutoAnkiPlugin extends Plugin {
settings: PluginSettings;
leafId: string;
statusBar: StatusBarElement;
statusBarIcon: HTMLElement;

async onload() {
await this.loadSettings();
Expand All @@ -32,29 +35,64 @@ export default class AutoAnkiPlugin extends Plugin {
const defaults = this.settings.questionGenerationDefaults;
const { textSelection: defaultsTextSelection, file: defaultsFile } = defaults;

this.statusBar = this.addStatusBarItem();
this.statusBar.className = 'status-bar-auto-anki'
this.statusBarIcon = this.statusBar.createEl('span', { cls: 'status-bar-icon' });
this.statusBar.createEl('div', { text: 'auto-anki' });
this.statusBar.doReset = () => {
setIcon(this.statusBarIcon, 'check-circle-2');
this.statusBar.classList.remove('--running');
this.statusBar.classList.remove('--error');
};
this.statusBar.doDisplayError = () => {
setIcon(this.statusBarIcon, 'alert-circle');
this.statusBar.classList.remove('--running');
this.statusBar.classList.add('--error');

}
this.statusBar.doDisplayRunning = () => {
setIcon(this.statusBarIcon, 'refresh-cw');
this.statusBar.classList.remove('--error');
this.statusBar.classList.add('--running');
};
this.statusBar.doReset();

this.addCommand({
id: 'export-current-file-to-anki',
name: 'Export Current File to Anki',
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
if (this.settings.openAiApiKey != null && view.data.length > 0) {
if (!checking) {
// const apiKey = electronDecrypt(this.settings.openAiApiKey);
const apiKey = this.settings.openAiApiKey;
const port = this.settings.ankiConnectPort || ANKI_CONNECT_DEFAULT_PORT;
new ExportModal(
this.app,
view.data,
apiKey,
port,
this.settings.ankiDestinationDeck,
this.settings.gptAdvancedOptions,
defaultsFile.numQuestions,
defaultsFile.numAlternatives,
).open();
checkCallback: (checking: boolean) => {
if (this.settings.openAiApiKey == null) {
return false;
}

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view == null) {
return false;
}

if (!checking) {
if (view.data.length <= 0) {
new Notice('There is nothing in the file!');
return;
}
return true

// const apiKey = electronDecrypt(this.settings.openAiApiKey);
const apiKey = this.settings.openAiApiKey;
const port = this.settings.ankiConnectPort || ANKI_CONNECT_DEFAULT_PORT;
new ExportModal(
this.app,
this.statusBar,
view.data,
apiKey,
port,
this.settings.ankiDestinationDeck,
this.settings.gptAdvancedOptions,
defaultsFile.numQuestions,
defaultsFile.numAlternatives,
).open();
}
return false;

return true;
},
});

Expand All @@ -63,25 +101,34 @@ export default class AutoAnkiPlugin extends Plugin {
name: 'Export Highlighted Text to Anki',
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
const currTextSelection = editor.getSelection();
if (this.settings.openAiApiKey != null && currTextSelection.length > 0) {
if (!checking) {
// const apiKey = electronDecrypt(this.settings.openAiApiKey);
const apiKey = this.settings.openAiApiKey;
const port = this.settings.ankiConnectPort || ANKI_CONNECT_DEFAULT_PORT;
new ExportModal(
this.app,
currTextSelection,
apiKey,
port,
this.settings.ankiDestinationDeck,
this.settings.gptAdvancedOptions,
defaultsTextSelection.numQuestions,
defaultsTextSelection.numAlternatives,
).open();

if (this.settings.openAiApiKey == null) {
return false;
}

if (!checking) {
if (currTextSelection.length == 0) {
new Notice('No text was selected!');
return;
}
return true;

// const apiKey = electronDecrypt(this.settings.openAiApiKey);
const apiKey = this.settings.openAiApiKey;
const port = this.settings.ankiConnectPort || ANKI_CONNECT_DEFAULT_PORT;
new ExportModal(
this.app,
this.statusBar,
currTextSelection,
apiKey,
port,
this.settings.ankiDestinationDeck,
this.settings.gptAdvancedOptions,
defaultsTextSelection.numQuestions,
defaultsTextSelection.numAlternatives,
).open();
}
return false;

return true;
},
});
}
Expand Down Expand Up @@ -114,7 +161,7 @@ class AutoAnkiSettingTab extends PluginSettingTab {

const ankiDescription = document.createElement('div');
// use innerHTML for harcoded description
ankiDescription.innerHTML = '<p><a href="https://apps.ankiweb.net/">Anki</a> is an open-source flashcard program that is popular for spaced repetition. This plugin has only been tested on desktop, and requires <a href="https://foosoft.net/projects/anki-connect/">Anki Connect</a> to be installed alongside the main Anki program.</p><p>Enabling this plugin will add commands to automatically generate Question-Answer-style flashcards into the Anki system using OpenAI\'s AI models.</p>';
ankiDescription.innerHTML = '<p><a href="https://apps.ankiweb.net/">Anki</a> is an open-source flashcard program that is popular for spaced repetition. This plugin has only been tested on desktop, and requires <a href="https://foosoft.net/projects/anki-connect/">Anki Connect</a> to be installed alongside the main Anki program.</p><p>Enabling this plugin will add commands to automatically generate Question-Answer-style flashcards into the Anki system using OpenAI\'s AI models.</p><p>For information on usage, see <a href="https://github.com/ad2969/obsidian-auto-anki#readme">the instructions</a> online.</p>';
containerEl.appendChild(ankiDescription)

new Setting(containerEl)
Expand Down Expand Up @@ -254,8 +301,10 @@ class AutoAnkiSettingTab extends PluginSettingTab {

// See OpenAI docs for more info:
// https://platform.openai.com/docs/api-reference/completions
const tempValComponent = createEl('span', { text: String(this.plugin.settings.gptAdvancedOptions.temperature) });
tempValComponent.className = 'slider-val'; // used to make custom slider component with displayed value next to it
const tempValComponent = createEl('span', {
text: String(this.plugin.settings.gptAdvancedOptions.temperature),
cls: 'slider-val', // used to make custom slider component with displayed value next to it
});
const tempComponent = new Setting(containerEl)
.setName('Temperature')
.setDesc('The sampling temperature used. Higher values increases randomness, while lower values makes the output more deterministic. (Default = 1)')
Expand All @@ -270,8 +319,10 @@ class AutoAnkiSettingTab extends PluginSettingTab {
);
tempComponent.settingEl.appendChild(tempValComponent);

const topPValComponent = createEl('span', { text: String(this.plugin.settings.gptAdvancedOptions.top_p) });
topPValComponent.className = 'slider-val';
const topPValComponent = createEl('span', {
text: String(this.plugin.settings.gptAdvancedOptions.top_p),
cls: 'slider-val',
});
const topPComponent = new Setting(containerEl)
.setName('Top P')
.setDesc('Value for nucleus sampling. Lower values mean the output considers the tokens comprising higher probability mass. (Default = 1)')
Expand All @@ -286,8 +337,10 @@ class AutoAnkiSettingTab extends PluginSettingTab {
);
topPComponent.settingEl.appendChild(topPValComponent);

const fPenaltyValComponent = createEl('span', { text: String(this.plugin.settings.gptAdvancedOptions.frequency_penalty) });
fPenaltyValComponent.className = 'slider-val';
const fPenaltyValComponent = createEl('span', {
text: String(this.plugin.settings.gptAdvancedOptions.frequency_penalty),
cls: 'slider-val',
});
const fPenaltyComponent = new Setting(containerEl)
.setName('Frequency Penalty')
.setDesc('Positive values penalize new tokens based on their existing frequency in the text so far. Higher values decrease chance of \'repetition\'. (Default = 0)')
Expand All @@ -302,8 +355,10 @@ class AutoAnkiSettingTab extends PluginSettingTab {
);
fPenaltyComponent.settingEl.appendChild(fPenaltyValComponent);

const pPenaltyValComponent = createEl('span', { text: String(this.plugin.settings.gptAdvancedOptions.presence_penalty) });
pPenaltyValComponent.className = 'slider-val';
const pPenaltyValComponent = createEl('span', {
text: String(this.plugin.settings.gptAdvancedOptions.presence_penalty),
cls: 'slider-val',
});
const pPenaltyComponent = new Setting(containerEl)
.setName('Presence Penalty')
.setDesc('Positive values penalize new tokens based on whether they appear in the text so far. Higher values increase chance of \'creativity\'. (Default = 0)')
Expand Down
Loading

0 comments on commit 007998e

Please sign in to comment.