mirror of
https://github.com/aidantilgner/AutogenObsidianPlugin.git
synced 2026-07-22 09:20:32 +00:00
feat: got plugin to work somewhat
This commit is contained in:
parent
4baf76b940
commit
aaf2069cb1
5 changed files with 2856 additions and 92 deletions
325
main.ts
325
main.ts
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
2521
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
87
utils/openai.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue