mirror of
https://github.com/yzh503/obsidian-aicommander-plugin.git
synced 2026-07-22 07:40:26 +00:00
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting} from 'obsidian';
|
|
import { PromptModal } from "./modal";
|
|
import { Configuration, OpenAIApi, CreateImageRequestSizeEnum } from "openai";
|
|
import axios from 'axios';
|
|
|
|
interface MyPluginSettings {
|
|
model: string;
|
|
apiKey: string;
|
|
imgSize: string;
|
|
useSearchEngine: boolean;
|
|
searchEngine: string;
|
|
bingSearchKey: string;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
model: 'gpt-3.5-turbo',
|
|
apiKey: '',
|
|
imgSize: '256x256',
|
|
useSearchEngine: false,
|
|
searchEngine: 'bing',
|
|
bingSearchKey: ''
|
|
}
|
|
|
|
export default class MyPlugin extends Plugin {
|
|
settings: MyPluginSettings;
|
|
|
|
async generateText(prompt: string) {
|
|
|
|
if (prompt.length < 1 || prompt.length > 4096) return({
|
|
success: false,
|
|
prompt: prompt,
|
|
text: 'Prompt needs to be longer than 1 and shorter than 4096 characters.'
|
|
})
|
|
|
|
const configuration = new Configuration({
|
|
apiKey: this.settings.apiKey,
|
|
});
|
|
const openai = new OpenAIApi(configuration);
|
|
|
|
let completion;
|
|
|
|
if (this.settings.useSearchEngine) {
|
|
console.log('searchResult');
|
|
|
|
const searchResult = await this.searchText(prompt);
|
|
console.log(searchResult);
|
|
completion = await openai.createChatCompletion({
|
|
model: this.settings.model,
|
|
messages: [{
|
|
role: 'system',
|
|
content: 'You are an assistant who can combine web search results when answering questions. You will receive some search results from a web search API. Add the information from the results to your answer, and mention the source.'
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
content: JSON.stringify(searchResult)
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: prompt
|
|
}],
|
|
});
|
|
} else {
|
|
completion = await openai.createChatCompletion({
|
|
model: this.settings.model,
|
|
messages: [{
|
|
role: 'user',
|
|
content: prompt
|
|
}],
|
|
});
|
|
}
|
|
|
|
console.log(completion.data);
|
|
|
|
const res = completion.data.choices[0].message
|
|
let message;
|
|
if (res == undefined) message = 'No response from model.';
|
|
else message = res.content;
|
|
|
|
return({
|
|
success: true,
|
|
text: message,
|
|
prompt: prompt
|
|
});
|
|
}
|
|
|
|
async getImageBase64(url: string) {
|
|
return fetch(url)
|
|
.then(response => response.blob())
|
|
.then(blob => {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
resolve(reader.result);
|
|
};
|
|
reader.onerror = () => {
|
|
reject(new Error("Failed to convert image to base64"));
|
|
};
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
});
|
|
}
|
|
|
|
async generateImage(prompt: string) {
|
|
|
|
if (prompt.length > 1000) return({
|
|
success: false,
|
|
prompt: prompt,
|
|
text: 'Prompt needs to be shorter than 1000 characters.'
|
|
})
|
|
|
|
const configuration = new Configuration({
|
|
apiKey: this.settings.apiKey,
|
|
});
|
|
const openai = new OpenAIApi(configuration);
|
|
const response = await openai.createImage({
|
|
prompt: prompt,
|
|
n: 1,
|
|
size: this.settings.imgSize as CreateImageRequestSizeEnum,
|
|
response_format: 'b64_json'
|
|
});
|
|
|
|
return({
|
|
success: true,
|
|
prompt: prompt,
|
|
text: `\n\n`
|
|
})
|
|
}
|
|
|
|
async generateTranscript(audioBuffer: ArrayBuffer, filetype: string, path: string) {
|
|
|
|
// Workaround for issue https://github.com/openai/openai-node/issues/77
|
|
const baseUrl = 'https://api.openai.com/v1/audio/transcriptions';
|
|
|
|
const blob = new Blob([audioBuffer]);
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', blob, 'audio.' + filetype);
|
|
formData.append('model', 'whisper-1');
|
|
|
|
let result;
|
|
|
|
await axios.post(baseUrl, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
'Authorization': 'Bearer ' + this.settings.apiKey
|
|
}
|
|
}).then(response => {
|
|
console.log(response.data);
|
|
result = response.data.text;
|
|
})
|
|
.catch(error => {
|
|
result = error.response.data.error.message;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
findAudioFilePath(editor: Editor) {
|
|
const position = editor.getCursor();
|
|
const markdownText = editor.getRange({line: 0, ch: 0}, position);
|
|
const regex = /!\[\[(([^[\]])+)\.(mp3|mp4|mpeg|mpga|m4a|wav|webm)\]\]/g;
|
|
|
|
let filename = '';
|
|
let result: RegExpExecArray | null;
|
|
while ((result = regex.exec(markdownText)) !== null) {
|
|
filename = result[0].slice(3, -2);
|
|
}
|
|
|
|
return filename;
|
|
}
|
|
|
|
async searchText(prompt: string) {
|
|
const endpoint = 'https://api.bing.microsoft.com/v7.0/search';
|
|
const subscriptionKey = this.settings.bingSearchKey;
|
|
|
|
let values;
|
|
await axios.get(endpoint, {
|
|
headers: {
|
|
'Ocp-Apim-Subscription-Key': subscriptionKey
|
|
},
|
|
params: {
|
|
q: prompt,
|
|
}
|
|
})
|
|
.then(response => {
|
|
values = response.data.webPages.value;
|
|
})
|
|
.catch(error => {
|
|
console.log(error);
|
|
});
|
|
|
|
return values;
|
|
}
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
this.addCommand({
|
|
id: 'prompt-text',
|
|
name: 'Generate text from prompt',
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
const onSubmit = (prompt: string) => {
|
|
const position = editor.getCursor();
|
|
new Notice("Generating text...");
|
|
this.generateText(prompt).then((value) => {
|
|
if (value.success) {
|
|
new Notice('Text Generated.');
|
|
editor.setLine(position.line, `${value.text}`);
|
|
} else {
|
|
new Notice(value.text);
|
|
}
|
|
});
|
|
};
|
|
new PromptModal(this.app, "", onSubmit).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'prompt-text-line',
|
|
name: 'Generate text from current line',
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
const position = editor.getCursor();
|
|
new Notice("Generating text...");
|
|
this.generateText(editor.getLine(position.line)).then((value) => {
|
|
if (value.success) {
|
|
new Notice('Text Generated.');
|
|
editor.setLine(position.line, `${value.prompt}\n\n${value.text}`);
|
|
} else {
|
|
new Notice(value.text);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'prompt-img',
|
|
name: 'Generate image from prompt',
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
const onSubmit = (prompt: string) => {
|
|
const position = editor.getCursor();
|
|
new Notice("Generating image...");
|
|
this.generateImage(prompt).then((value) => {
|
|
if (value.success) {
|
|
new Notice('Image Generated.');
|
|
editor.setLine(position.line, `${value.text}`);
|
|
} else {
|
|
new Notice(value.text);
|
|
}
|
|
});
|
|
};
|
|
new PromptModal(this.app, "", onSubmit).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'prompt-img-line',
|
|
name: 'Generate image from current line',
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
const position = editor.getCursor();
|
|
new Notice("Generating image...");
|
|
this.generateImage(editor.getLine(position.line)).then((value) => {
|
|
if (value.success) {
|
|
new Notice('Image Generated.');
|
|
editor.setLine(position.line, `${value.prompt}${value.text}`);
|
|
} else {
|
|
new Notice(value.text);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'audio-transcript',
|
|
name: 'Generate a transcript from the above audio',
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
const position = editor.getCursor();
|
|
const line = editor.getLine(position.line)
|
|
const path = this.findAudioFilePath(editor);
|
|
const fileType = path.split('.').pop();
|
|
|
|
if (fileType === undefined) {
|
|
new Notice('No audio file found');
|
|
return;
|
|
}
|
|
this.app.vault.adapter.readBinary(path).then((audioBuffer) => {
|
|
new Notice("Generating transcript...");
|
|
this.generateTranscript(audioBuffer, fileType, path).then((result) => {
|
|
new Notice('Transcript Generated.');
|
|
editor.setLine(position.line, `${line}\n\n${result}`);
|
|
});
|
|
});
|
|
|
|
}
|
|
});
|
|
|
|
// This adds a settings tab so the user can configure various aspects of the plugin
|
|
this.addSettingTab(new ApiSettingTab(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));
|
|
}
|
|
|
|
onunload() {
|
|
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
class ApiSettingTab extends PluginSettingTab {
|
|
plugin: MyPlugin;
|
|
|
|
constructor(app: App, plugin: MyPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const {containerEl} = this;
|
|
containerEl.empty();
|
|
containerEl.createEl('h2', {text: 'Settings'});
|
|
containerEl.createEl('h3', {text: 'OpenAI API'});
|
|
|
|
new Setting(containerEl)
|
|
.setName('OpenAI API key')
|
|
.setDesc('For use of OpenAI models')
|
|
.addText(text => text
|
|
.setPlaceholder('Enter your key')
|
|
.setValue(this.plugin.settings.apiKey)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.apiKey = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Text Model')
|
|
.setDesc('Select the model to use for text generation')
|
|
.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();
|
|
}));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Image Size')
|
|
.setDesc('Size of the image to generate')
|
|
.addDropdown(dropdown => dropdown
|
|
.addOption('256x256', '256x256')
|
|
.addOption('512x512', '512x512')
|
|
.addOption('1024x1024', '1024x1024')
|
|
.setValue(this.plugin.settings.imgSize)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.imgSize = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
containerEl.createEl('h3', {text: 'Search Engine'});
|
|
|
|
new Setting(containerEl)
|
|
.setName('Use search engine')
|
|
.setDesc("Use text generator with search engine")
|
|
.addToggle(value => value
|
|
.setValue(this.plugin.settings.useSearchEngine)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.useSearchEngine = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Search engine')
|
|
.setDesc("Select the search engine to use with text generator")
|
|
.addDropdown(dropdown => dropdown
|
|
.addOption('bing', 'bing')
|
|
.setValue(this.plugin.settings.searchEngine)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.searchEngine = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Bing Web Search API key')
|
|
.setDesc("Find in 'manage keys' in Azure portal")
|
|
.addText(text => text
|
|
.setPlaceholder('Enter your key')
|
|
.setValue(this.plugin.settings.bingSearchKey)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.bingSearchKey = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
}
|
|
}
|