feat: got plugin to work somewhat

This commit is contained in:
AidanTilgner 2024-02-01 13:24:18 -08:00
parent 4baf76b940
commit aaf2069cb1
5 changed files with 2856 additions and 92 deletions

325
main.ts
View file

@ -1,89 +1,169 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import {
App,
Editor,
MarkdownView,
Modal,
Plugin,
PluginSettingTab,
Setting,
} from "obsidian";
import OpenAI from "openai";
import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
import { getClient, getReplacement } from "utils/openai";
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
interface AutogenSettings {
openaiApiKey: string;
model: ChatCompletionCreateParamsBase["model"];
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
const DEFAULT_SETTINGS: AutogenSettings = {
openaiApiKey: "",
model: "gpt-3.5-turbo",
};
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
export default class Autogen extends Plugin {
settings: AutogenSettings;
typingTimeout: NodeJS.Timeout | null = null;
typingDelay = 2000;
openaiClient: OpenAI | null = null;
async onload() {
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.registerEvent(
this.app.workspace.on(
"editor-change",
this.handleEditorChange.bind(this)
)
);
// 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.addSettingTab(new AutogenSettingTab(this.app, this));
// 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;
}
}
});
// 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));
this.initOpenAIClient();
}
onunload() {
initOpenAIClient() {
if (this.settings.openaiApiKey) {
this.openaiClient = getClient(this.settings.openaiApiKey);
}
}
async handleEditorChange(editor: Editor, view: MarkdownView) {
if (this.typingTimeout !== null) {
clearTimeout(this.typingTimeout);
}
this.typingTimeout = setTimeout(
() => this.showConfirmationModal(editor),
this.typingDelay
);
}
async showConfirmationModal(editor: Editor) {
const pattern = /@\[(.*?)\]/g;
const content = editor.getValue();
const windowSize = 5000;
console.log("Editor change: ", content);
// Execute pattern search once instead of looping
const match = pattern.exec(content);
if (match !== null) {
console.log("Found match:", match[0]);
// Calculate window around the match
const start = Math.max(match.index - windowSize / 2, 0);
const end = Math.min(
match.index + match[0].length + windowSize / 2,
content.length
);
const textWindow = content.substring(start, end);
console.log("Text window: ", textWindow);
const onConfirm = async (modal: Modal) => {
modal.contentEl.empty();
modal.titleEl.setText("Generating replacement...");
const replacement = await this.generateReplacementText(
textWindow,
match[0]
);
modal.titleEl.setText("Replace selection with:");
const textEl = createEl("p", {
text: replacement,
});
modal.contentEl.appendChild(textEl);
modal.contentEl.createEl("br");
const confirmButton = createEl("button", { text: "Confirm" });
confirmButton.setCssStyles({
marginRight: "10px",
});
const cancelButton = createEl("button", { text: "Cancel" });
confirmButton.addEventListener("click", () => {
this.replaceText(editor, match[0], replacement);
modal.close();
});
cancelButton.addEventListener("click", () => {
modal.close();
});
modal.contentEl.appendChild(confirmButton);
};
const modal = new AutogenConfirmationModal(
this.app,
match[0],
onConfirm
);
modal.open();
}
}
async generateReplacementText(window: string, match: string) {
if (this.openaiClient === null) {
this.initOpenAIClient();
}
if (this.openaiClient !== null) {
const replacement = await getReplacement(
this.openaiClient,
this.settings.model,
window,
match
);
if (replacement) {
return replacement;
} else {
console.error("Error generating replacement text");
return "Error: Error generating replacement text";
}
} else {
console.error("OpenAI client not initialized");
return "Error: OpenAI client not initialized. Please make sure there is an API key set in the settings.";
}
}
replaceText(editor: Editor, match: string, replacement: string) {
// replace the match with the replacement
const content = editor.getValue();
const newContent = content.replace(match, replacement);
editor.setValue(newContent);
}
onunload() {}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
@ -91,44 +171,115 @@ export default class MyPlugin extends Plugin {
}
}
class SampleModal extends Modal {
constructor(app: App) {
class AutogenConfirmationModal extends Modal {
public match = "";
public onConfirm: (modal: Modal) => void;
constructor(app: App, match: string, onConfirm: (modal: Modal) => void) {
super(app);
this.match = match;
this.onConfirm = onConfirm;
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
const { contentEl, titleEl } = this;
titleEl.setText("Replace this selection?");
const textEl = createEl("p", {
text: this.match.replace("@[", "").replace("]", ""),
});
contentEl.appendChild(textEl);
contentEl.createEl("br");
const confirmButton = createEl("button", { text: "Yes" });
confirmButton.setCssStyles({
marginRight: "10px",
});
const cancelButton = createEl("button", { text: "No" });
confirmButton.addEventListener("click", async () => {
contentEl.empty();
this.onConfirm(this);
});
cancelButton.addEventListener("click", () => {
this.close();
});
contentEl.appendChild(confirmButton);
contentEl.appendChild(cancelButton);
}
onClose() {
const {contentEl} = this;
const { contentEl } = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
class AutogenSettingTab extends PluginSettingTab {
plugin: Autogen;
constructor(app: App, plugin: MyPlugin) {
constructor(app: App, plugin: Autogen) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
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) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
.setName("OpenAI API Key")
.setDesc(
"Your OpenAI API Key (find or create one at https://platform.openai.com/api-keys)"
)
.addText((text) =>
text
.setPlaceholder("Enter your API Key")
.setValue(this.plugin.settings.openaiApiKey)
.onChange(async (value) => {
this.plugin.settings.openaiApiKey = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Model")
.setDesc("The model to use for generating text")
.addDropdown((dropdown) => {
dropdown
.addOption("gpt-3.5-turbo", "GPT-3.5 Turbo")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-3.5-turbo-16k", "GPT-3.5 Turbo 16k")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-4", "GPT-4")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
dropdown
.addOption("gpt-4-32k", "GPT-4 32k")
.setValue(this.plugin.settings.model)
.onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
});
}
}

View file

@ -1,11 +1,11 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"id": "obsidian-autogen-plugin",
"name": "Autogen",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"description": "In place autogeneration of content based on prompts.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"authorUrl": "https://github.com/AidanTilgner",
"fundingUrl": "https://www.buymeacoffee.com/aidantilgner",
"isDesktopOnly": false
}

2521
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -20,5 +20,10 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"@types/dotenv": "^8.2.0",
"dotenv": "^16.4.1",
"openai": "^4.26.0"
}
}

87
utils/openai.ts Normal file
View file

@ -0,0 +1,87 @@
import OpenAI from "openai";
import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
export const getClient = (apiKey: string) => {
console.log("Dangerously allowing browser");
return new OpenAI({
apiKey,
dangerouslyAllowBrowser: true,
});
};
export const getReplacement = async (
client: OpenAI,
model: ChatCompletionCreateParamsBase["model"],
textWindow: string,
match: string
) => {
try {
const response = await client.chat.completions.create({
model,
messages: [
{
content: `
You are a helpful text replacer. Given a selection of text, you are tasked with generating a replacement for the selection.
The selection will be shown in the following format:
@[selection]
And its your job to generate what should go there in replacement, based on the user's prompt.
`,
role: "system",
},
{
content: `
Full Text:
${textWindow}
Specific Selection to Replace:
${match}
`,
role: "user",
},
],
tools: [
{
type: "function",
function: {
name: "replace_text",
description:
"Replace the selection based on its content",
parameters: {
type: "object",
properties: {
selectionReplacement: {
type: "string",
description:
"The text that will replace the selection",
},
},
},
},
},
],
tool_choice: {
type: "function",
function: {
name: "replace_text",
},
},
});
if (response.choices.length === 0) {
return "Error: No response from OpenAI";
}
const call = response.choices[0].message.tool_calls?.[0].function;
if (call === undefined) {
return "Error: Invalid response from OpenAI";
}
const replacement = JSON.parse(call.arguments).selectionReplacement;
if (replacement === undefined) {
return "Error: Invalid response from OpenAI";
}
return replacement;
} catch (e) {
console.error(e);
return undefined;
}
};