binxly_Atomizer/main.ts

333 lines
11 KiB
TypeScript

import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, normalizePath } from 'obsidian';
import OpenAI from 'openai';
// Types and Interfaces
interface APIError extends Error {
response?: {
status: number;
data: any;
};
}
interface AtomicPluginSettings {
apiKey: string;
outputFolder: string;
model: string;
enableAtomizedTag: boolean;
customTags: string;
}
// Constants
const DEFAULT_SETTINGS: AtomicPluginSettings = {
apiKey: '',
outputFolder: 'atomic-notes',
model: 'gpt-4o-mini',
enableAtomizedTag: true,
customTags: ''
}
const getCurrentDateTime = (): string => {
const date = new Date();
return date.toISOString()
.replace('T', ' ')
.replace(/\.\d+Z$/, '');
};
// Move SYSTEM_PROMPT into a function that takes settings as a parameter
const getSystemPrompt = (settings: AtomicPluginSettings): string => {
return `You are an expert at creating atomic notes from a single, larger note.
Take the content from a larger note and break it down into separate compact yet detailed atomic notes. Each note MUST be separated by placing '<<<>>>' on its own line between notes. Do not include an index or main note. Follow these rules:
1. Each note should contain exactly one clear idea. This can contain multiple lines.
2. Each note must have a YAML frontmatter section at the top with:
---
date: "${getCurrentDateTime()}"
tags: ${settings.enableAtomizedTag ? 'atomized' : ''}${settings.customTags ? (settings.enableAtomizedTag ? ', ' : '') + settings.customTags : ''}
---
3. You MUST separate each note by placing '<<<>>>' on its own line between notes
4. After the frontmatter, each note must start with a level 1 heading (# Title)
5. The content should be self-contained and independently understandable
6. Use proper Markdown formatting
7. Do not include the separator at the start or end of the response`;
};
// OpenAI Service
class OpenAIService {
constructor(private apiKey: string, private model: string, private settings: AtomicPluginSettings) {}
async generateAtomicNotes(content: string, timestamp: string): Promise<string> {
if (!this.apiKey) {
new Notice('OpenAI API key is not set. Please configure it in plugin settings.');
return '';
}
// Inform user about network request
new Notice('Sending request to OpenAI...', 3000);
const openai = new OpenAI({
apiKey: this.apiKey,
dangerouslyAllowBrowser: true
});
const completion = await openai.chat.completions.create({
model: this.model,
messages: [{
role: "system",
content: getSystemPrompt(this.settings)
}, {
role: "user",
content: content
}],
temperature: 0.7,
max_tokens: 4000
});
return completion.choices[0]?.message?.content ?? '';
}
}
// Notes Manager
class NotesManager {
private usedTitles = new Set<string>();
constructor(private app: App, private folderPath: string) {}
async createFolder(): Promise<void> {
if (!(await this.app.vault.adapter.exists(this.folderPath))) {
await this.app.vault.createFolder(this.folderPath);
}
}
async saveNote(note: string): Promise<void> {
const titleMatch = note.match(/^#\s+(.+)$/m);
if (!titleMatch) {
console.warn('No title found in note:', note.slice(0, 100));
return;
}
const title = this.getUniqueTitle(titleMatch[1].trim());
const fileName = `${this.folderPath}/${normalizePath(title)}.md`;
await this.app.vault.create(fileName, note.trim());
}
private getUniqueTitle(baseTitle: string): string {
let title = baseTitle;
let counter = 1;
while (this.usedTitles.has(title)) {
title = `${baseTitle} ${counter}`;
counter++;
}
return normalizePath(title);
}
}
// Main Plugin Class
export default class AtomicNotesPlugin extends Plugin {
settings: AtomicPluginSettings;
private statusBarItem: HTMLElement;
async onload() {
await this.loadSettings();
// Add ribbon icon
this.addRibbonIcon('atom', 'Atomize note', async () => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
await this.atomizeNote(activeView);
} else {
new Notice('Please open a note to atomize');
}
});
// Add command
this.addCommand({
id: 'atomize-current-note',
name: 'Atomize current note',
editorCallback: async (editor: Editor, view: MarkdownView) => {
await this.atomizeNote(view);
}
});
// Add settings tab
this.addSettingTab(new AtomicSettingTab(this.app, this));
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.setText('Atomic notes: ready');
}
async atomizeNote(view: MarkdownView) {
if (!this.validateSettings()) return;
const content = view.getViewData();
if (!content.trim()) {
new Notice('The current note is empty');
return;
}
new ConfirmationModal(this.app, async () => {
const loadingNotice = new Notice('Processing note...', 0);
try {
const openAIService = new OpenAIService(
this.settings.apiKey,
this.settings.model,
this.settings
);
const notesManager = new NotesManager(this.app, this.settings.outputFolder);
const responseContent = await openAIService.generateAtomicNotes(content, this.getCurrentDateTime());
if (!responseContent) throw new Error('No content received from OpenAI');
const atomicNotes = responseContent
.split('<<<>>>')
.map(note => note.trim())
.filter(note => note.length > 0);
if (atomicNotes.length === 0) {
throw new Error('No atomic notes were generated. The response may not be properly formatted.');
}
await notesManager.createFolder();
for (const note of atomicNotes) {
await notesManager.saveNote(note);
}
new Notice(`Successfully created ${atomicNotes.length} atomic notes`);
} catch (error) {
this.handleError(error);
} finally {
loadingNotice.hide();
this.statusBarItem.setText('Atomic Notes: Ready');
}
}).open();
}
private validateSettings(): boolean {
if (!this.settings.apiKey) {
new Notice('Please set your OpenAI API key in settings');
return false;
}
return true;
}
private handleError(error: APIError) {
console.error('Detailed error:', error);
new Notice(`Error: ${error.message || 'Unknown error occurred'}`);
if (error.response) {
console.error('OpenAI API Error:', {
status: error.response.status,
data: error.response.data
});
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
private getCurrentDateTime(): string {
return new Date().toISOString();
}
}
class AtomicSettingTab extends PluginSettingTab {
plugin: AtomicNotesPlugin;
constructor(app: App, plugin: AtomicNotesPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('OpenAI API key')
.setDesc('Enter your OpenAI API key')
.addText(text => text
.setPlaceholder('sk-...')
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Model')
.setDesc('Choose the OpenAI model to use')
.addDropdown(dropdown => dropdown
.addOption('gpt-4o-mini', 'GPT-4o Mini')
.addOption('gpt-4o', 'GPT-4o')
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Output folder')
.setDesc('Folder where atomic notes will be created')
.addText(text => text
.setPlaceholder('atomic-notes')
.setValue(this.plugin.settings.outputFolder)
.onChange(async (value) => {
this.plugin.settings.outputFolder = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable "atomized" tag')
.setDesc('Automatically add the "atomized" tag to generated notes')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAtomizedTag)
.onChange(async (value) => {
this.plugin.settings.enableAtomizedTag = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Custom tags')
.setDesc('Additional tags to add to generated notes (comma-separated)')
.addText(text => text
.setPlaceholder('tag1, tag2, tag3')
.setValue(this.plugin.settings.customTags)
.onChange(async (value) => {
this.plugin.settings.customTags = value;
await this.plugin.saveSettings();
}));
}
}
class ConfirmationModal extends Modal {
constructor(app: App, private onConfirm: () => void) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('This will create multiple atomic notes in your vault. Continue?');
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => this.close()))
.addButton(btn => btn
.setButtonText('Continue')
.setCta()
.onClick(() => {
this.close();
this.onConfirm();
}));
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}