Compare commits

..

30 commits

Author SHA1 Message Date
Simon Yang
4803788959 Update version 2024-10-15 22:29:09 +11:00
Simon Yang
60c3f51d2c Fix prompt perfect 2024-10-15 22:26:55 +11:00
Simon Yang
330ce44d20 Update version 2024-10-15 21:42:49 +11:00
Simon Yang
e2b46a8f2e Fix issues. Improve robustness. 2024-10-15 21:37:19 +11:00
Simon Yang
a6fb4afbb6 Update version number 2023-04-25 17:16:09 +10:00
Simon Yang
6750ad7062 Improve description 2023-04-25 17:11:08 +10:00
Simon Yang
e6af43e076 Support Bing without an API key 2023-04-25 17:09:11 +10:00
Simon Yang
71edf59e51 Improve pdf prompt performance 2023-04-25 16:18:43 +10:00
Simon Yang
174cb2fc47
Update README.md 2023-03-29 11:42:32 +11:00
Simon Yang
2fa22c6c88 Update version 2023-03-29 11:32:43 +11:00
Simon Yang
64b6f35f55 Support any model in settings. 2023-03-29 11:32:21 +11:00
Simon Yang
7a5f9c14a4 Add demo 2023-03-18 20:58:31 +11:00
Simon Yang
8eebc1c572 Improve search performance 2023-03-18 10:19:06 +11:00
Simon Yang
b6bbb83b47 Improve settings description 2023-03-18 10:12:42 +11:00
Simon Yang
3e80d04c40 Improve error messages 2023-03-18 10:10:01 +11:00
Simon Yang
85e2da4f52 Display correct error message 2023-03-18 10:01:28 +11:00
Simon Yang
a2454d06ac Update version. 2023-03-18 09:13:14 +11:00
Simon Yang
d951df8e3d Clean up 2023-03-17 23:50:23 +11:00
Simon Yang
0e64454235 Add writing status check 2023-03-17 23:49:59 +11:00
Simon Yang
0e6911c8ff Support streaming text. Fix bugs 2023-03-17 22:40:20 +11:00
Simon Yang
ae1a176893 Fix search result 2023-03-16 19:03:17 +11:00
Simon Yang
c7248e6957 Clean up 2023-03-16 18:37:04 +11:00
Simon Yang
5fcd2ec676 Clean packages 2023-03-16 10:50:07 +11:00
Simon Yang
85fff866fa Improve code quality. 2023-03-16 04:09:43 +11:00
Simon Yang
aa6d48b68c Update version 2023-03-15 17:10:36 +11:00
Simon Yang
592f819a47 Fix a bug of text insertion with selected text 2023-03-15 17:10:17 +11:00
Simon Yang
9a62867e62 Update version 2023-03-15 15:24:07 +11:00
Simon Yang
5876c5a9e7 Support auto saving image as attachment 2023-03-15 14:45:25 +11:00
Simon Yang
778db2ec16 Enhance gpt quality using system mode 2023-03-15 13:59:23 +11:00
Simon Yang
cbb78ac5ce Update user doc 2023-03-15 11:35:11 +11:00
12 changed files with 1277 additions and 4232 deletions

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 100
}

View file

@ -2,7 +2,9 @@
<a href="https://www.buymeacoffee.com/yzh503" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 150px !important;" ></a>
**AI Commander** is a simple AI connector that helps you write notes with the power of OpenAI APIs in conjunction with Bing search engine.
**AI Commander** is a simple AI connector that helps you write notes with the power of OpenAI (ChatGPT) APIs in conjunction with Bing search engine.
![Demo](demo.gif)
[OpenAI API key](https://platform.openai.com/account/api-keys) is required to use this plugin.
@ -14,13 +16,13 @@
- Generate text in conjunction with search engine results. This allows the text model to work with Bing search engine. A [Bing Web Search API](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) key is required to use this feature.
- Generate text in context of the **PDF** attachment embedded in the note. In this mode, it will not incorporate the search result.
- Use [Prompt Perfect](https://promptperfect.jina.ai/) to automatically improve prompts. Your prompts will be replaced by the improved one.
- Custom prompt commands for selected text and PDF. For example, add a command "Summarise the text" in the plugin settings, and it will appear as a command. Note that you need to **reload** the plugin by disabling and re-enabling it after changing the Custom Commands setting.
- Custom prompt commands for selected text and PDF. For example, add a command "Summarise the text" in the plugin settings, and it will appear as a command. **Note that you need to reload the plugin and restart Obsidian to update the command palette**.
## Supported models
- OpenAI gpt-3.5-turbo
- OpenAI Whisper
- OpenAI image generation model
- GPT models listed in https://platform.openai.com/docs/models/gpt-4
- Whisper v1
- DALL·E image generation model
## How to use

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -15,7 +15,7 @@ const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",

664
main.ts
View file

@ -1,664 +0,0 @@
import { App, Editor, MarkdownView, normalizePath, Notice, Plugin, PluginSettingTab, Setting, loadPdfJs} from 'obsidian';
import { PromptModal } from "./modal";
import { Configuration, OpenAIApi, CreateImageRequestSizeEnum, ChatCompletionRequestMessage } from "openai";
import axios from 'axios';
interface AICommanderPluginSettings {
model: string;
apiKey: string;
imgSize: string;
useSearchEngine: boolean;
searchEngine: string;
bingSearchKey: string;
usePromptPerfect: boolean;
promptPerfectKey: string;
promptsForSelected: string;
promptsForPdf: string
}
const DEFAULT_SETTINGS: AICommanderPluginSettings = {
model: 'gpt-3.5-turbo',
apiKey: '',
imgSize: '256x256',
useSearchEngine: false,
searchEngine: 'bing',
bingSearchKey: '',
promptPerfectKey: '',
usePromptPerfect: false,
promptsForSelected: '',
promptsForPdf: ''
}
export default class AICommanderPlugin extends Plugin {
settings: AICommanderPluginSettings;
async improvePrompt(prompt: string, targetModel: string) {
const YOUR_GENERATED_SECRET = '9VFMTHCukRuT5WqOkAD1:8cde275ebde49165527e9c97ecc96abef1e34473458fe8f9f3ecad177a163538';
const headers = {
'x-api-key': `token ${YOUR_GENERATED_SECRET}`,
'Content-Type': 'application/json'
};
const data = {
data: {
prompt: prompt,
targetModel: targetModel
}
};
const response = await axios.post('https://us-central1-prompt-ops.cloudfunctions.net/optimize', data, { headers })
if ('promptOptimized' in response.data.result) return response.data.result.promptOptimized as string;
else return prompt;
}
async generateText(prompt: string, contextPrompt?: string) {
if (prompt.length < 1 ) throw new Error('Cannot find prompt.');
if (this.settings.apiKey.length <= 1) throw new Error('OpenAI API Key is not provided.');
const configuration = new Configuration({ apiKey: this.settings.apiKey });
const openai = new OpenAIApi(configuration);
let newPrompt = prompt;
if (this.settings.usePromptPerfect) {
newPrompt = await this.improvePrompt(prompt, 'chatgpt');
}
const messages = [];
console.log(contextPrompt);
if (contextPrompt) {
messages.push({
role: 'user',
content: contextPrompt
});
} else if (this.settings.useSearchEngine) {
if (this.settings.bingSearchKey.length <= 1) throw new Error('Bing Search API Key is not provided.');
const searchResult = await this.searchText(prompt)
messages.push({
role: 'user',
content: 'As an assistant who can learn information from web search results, your task is to incorporate information from a web search API response into your answers when responding to questions. Your response should include the relevant information from the search API response and provide attribution by mentioning the source of information with the url. Please note that you should be able to handle various types of questions and search queries. Your response should also be clear and concise while incorporating all relevant information from the web search results. Here are the web search API response in JSON format: \n\n ' + JSON.stringify(searchResult)
});
}
messages.push({
role: 'user',
content: newPrompt
});
const data = {
model: this.settings.model,
messages: messages as ChatCompletionRequestMessage[],
};
console.log('Completion request: ', data);
const completion = await openai.createChatCompletion(data)
const message = completion.data.choices[0].message
if (!message) throw new Error('No response from OpenAI API');
const content = message.content;
return({
text: content,
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 < 1 ) throw new Error('Cannot find prompt.');
if (this.settings.apiKey.length <= 1) throw new Error('OpenAI API Key is not provided.');
const configuration = new Configuration({
apiKey: this.settings.apiKey,
});
const openai = new OpenAIApi(configuration);
let newPrompt = prompt;
if (this.settings.usePromptPerfect) {
newPrompt = await this.improvePrompt(prompt, 'dalle');
}
const response = await openai.createImage({
prompt: newPrompt,
n: 1,
size: this.settings.imgSize as CreateImageRequestSizeEnum,
response_format: 'b64_json'
});
const size = this.settings.imgSize.split('x')[0];
return({
prompt: prompt,
text: `![${size}](data:image/png;base64,${response.data.data[0].b64_json})\n`
})
}
async generateTranscript(audioBuffer: ArrayBuffer, filetype: string) {
if (this.settings.apiKey.length <= 1) throw new Error('OpenAI API Key is not provided.');
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');
return axios.post(baseUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': 'Bearer ' + this.settings.apiKey
}
}).then(response => response.data.text)
}
async searchText(prompt: string) {
const response = await axios.get('https://api.bing.microsoft.com/v7.0/search', {
headers: {
'Ocp-Apim-Subscription-Key': this.settings.bingSearchKey
},
params: {
q: prompt,
}
})
return response.data.webPages.value;
}
async getAttachmentDir() {
const attachmentFolder = await this.app.vault.adapter.read(`${this.app.vault.configDir}/app.json`).then((content: string) => {
const config = JSON.parse(content);
if ('attachmentFolderPath' in config) return config.attachmentFolderPath;
else return '';
});
return attachmentFolder as string;
}
// Test cases:
// 1. Attachment Folder: vault, Attachment: /audio.mp3
// 2. Attachment Folder: vault, Attachment: /folder/audio.mp3
// 3. Attachment Folder: specified, Attachment: /audio.mp3
// 4. Attachment Folder: specified, Attachment: /folder/audio.mp3
// 5. Attachment Folder: specified, Attachment: /specified/audio.mp3
// 6. Attachment Folder: same, Attachment: /audio.mp3
// 7. Attachment Folder: same, Attachment: /folder/audio.mp3
// 8. Attachment Folder: same, Attachment: /same/audio.mp3
// 9. Attachment Folder: subfolder, Attachment: /audio.mp3
// 10. Attachment Folder: subfolder, Attachment: /folder/audio.mp3
// 11. Attachment Folder: subfolder, Attachment: /same/subfolder/audio.mp3
// 12. Attachment Folder: subfolder, Attachment: /same/audio.mp3
async findFilePath(text: string, regex: RegExp[]) {
const filepath = await this.getAttachmentDir().then((attachmentPath) => {
let filename = '';
let result: RegExpExecArray | null;
for (const reg of regex) {
while ((result = reg.exec(text)) !== null) {
filename = normalizePath(decodeURI(result[0])).trim();
}
}
if (filename == '') throw new Error('No file found in the text.');
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) throw new Error('No active file');
const currentPath = activeFile.path.split('/');
currentPath.pop();
const currentPathString = currentPath.join('/');
console.log('currentPathString', currentPathString);
console.log('attachmentPath', attachmentPath);
console.log('filename', filename);
const underRootFolder = attachmentPath === '' || attachmentPath === '/';
const underCurrentFolder = attachmentPath.startsWith('./');
const underSpecificFolder = !underCurrentFolder && !underRootFolder;
const fileInSpecificFolder = filename.contains('/');
console.log(underRootFolder, underCurrentFolder, underSpecificFolder, fileInSpecificFolder);
let filepath = '';
if (underRootFolder || fileInSpecificFolder) filepath = filename;
if (underSpecificFolder) filepath = attachmentPath + '/' + filename;
if (underCurrentFolder) {
const attFolder = attachmentPath.substring(2);
if (attFolder.length == 0) filepath = currentPathString + '/' + filename;
else filepath = currentPathString + '/' + attFolder + '/' + filename;
}
return this.app.vault.adapter.exists(filepath).then((exists => {
if (exists) return filepath;
else {
let path = '';
let found = false;
this.app.vault.getFiles().forEach((file) => {
if (file.name === filename) {
path = file.path;
found = true;
}
});
if (found) return path;
else throw new Error('File not found');
}
}));
});
return filepath as string;
}
async generateTextWithPdf(prompt: string, filepath: string) {
const pdfBuffer = await this.app.vault.adapter.readBinary(filepath);
const pdfjs = await loadPdfJs();
const pdf = await pdfjs.getDocument(pdfBuffer).promise;
let totalContent = '';
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const content = await page.getTextContent();
const pageContent = content.items.map((item: any) => item.str).join(' ');
totalContent += `Page ${i + 1}: ` + pageContent.replace(/\s+/g, ' ') + '\n';
}
const context = `As an assistant who can learn from text given to you, your task is to incorporate information from text given to you into your answers when responding to questions. Your response should include the relevant information from the text given to you and provide attribution by mentioning the page number. Everything below is the text, which is extracted from a PDF file: \n\n${totalContent}`
return this.generateText(prompt, context);
}
getNextEmptyLine(editor: Editor) {
let line = editor.getCursor().line;
while (editor.getLine(line).trim() !== '') line++;
if (line == editor.lastLine()) {
editor.setLine(line, editor.getLine(line) + '\n');
line++;
}
return line;
}
processGeneratedText(editor: Editor, data: any) {
new Notice('Text Generated.');
const nextEmptyLine = this.getNextEmptyLine(editor);
editor.setLine(nextEmptyLine, '\n\n' + data.text.trim() + '\n\n');
}
commandGenerateText(editor: Editor, prompt: string) {
new Notice("Generating text...");
this.generateText(prompt).then((data) => {
this.processGeneratedText(editor, data);
}).catch(error => {
new Notice(error.message);
});
}
commandGenerateTextWithPdf(editor: Editor, prompt: string) {
const position = editor.getCursor();
const text = editor.getRange({line: 0, ch: 0}, position);
const regex = [/(?<=\[(.*)]\()(([^[\]])+)\.pdf(?=\))/g,
/(?<=\[\[)(([^[\]])+)\.pdf(?=]])/g];
this.findFilePath(text, regex).then((path) => {
console.log('path', path);
new Notice(`Generating text in context of ${path}...`);
this.generateTextWithPdf(prompt, path).then((data) => {
this.processGeneratedText(editor, data);
}).catch(error => {
new Notice(error.message);
});
}).catch(error => {
new Notice(error.message);
});
}
commandGenerateImage(editor: Editor, prompt: string) {
new Notice("Generating image...");
this.generateImage(prompt).then((data) => {
new Notice('Image Generated.');
this.processGeneratedText(editor, data);
}).catch(error => {
new Notice(error.message);
});
}
commandGenerateTranscript(editor: Editor) {
const position = editor.getCursor();
const line = editor.getLine(position.line)
const text = editor.getRange({line: 0, ch: 0}, position);
const regex = [/(?<=\[\[)(([^[\]])+)\.(mp3|mp4|mpeg|mpga|m4a|wav|webm)(?=]])/g,
/(?<=\[(.*)]\()(([^[\]])+)\.(mp3|mp4|mpeg|mpga|m4a|wav|webm)(?=\))/g];
this.findFilePath(text, regex).then((path) => {
const fileType = path.split('.').pop();
if (fileType == undefined || fileType == null || fileType == '') {
new Notice('No audio file found');
} else {
this.app.vault.adapter.exists(path).then((exists) => {
console.log('Audio filepath', path);
if (!exists) throw new Error(path + ' does not exist');
this.app.vault.adapter.readBinary(path).then((audioBuffer) => {
new Notice("Generating transcript...");
this.generateTranscript(audioBuffer, fileType).then((result) => {
new Notice('Transcript Generated.');
editor.setLine(position.line, `${line}${result}\n`);
});
}).catch(error => {
new Notice(error.message);
});
}).catch(error => {
new Notice(error.message);
});
}
}).catch(error => {
new Notice(error.message);
});
}
async onload() {
await this.loadSettings();
this.addCommand({
id: 'text-prompt',
name: 'Generate text from prompt',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateText(editor, prompt);
};
new PromptModal(this.app, "", onSubmit).open();
}
});
this.addCommand({
id: 'img-prompt',
name: 'Generate an image from prompt',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateImage(editor, prompt);
};
new PromptModal(this.app, "", onSubmit).open();
}
});
this.addCommand({
id: 'text-line',
name: 'Generate text from the current line',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineContent = editor.getLine(position.line);
this.commandGenerateText(editor, lineContent);
}
});
this.addCommand({
id: 'img-line',
name: 'Generate an image from the current line',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineContent = editor.getLine(position.line);
this.commandGenerateImage(editor, lineContent);
}
});
this.addCommand({
id: 'text-selected',
name: 'Generate text from the selected text',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
this.commandGenerateText(editor, selectedText);
}
});
this.addCommand({
id: 'img-selected',
name: 'Generate an image from the selected text',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
new Notice("Generating image...");
this.commandGenerateImage(editor, selectedText);
}
});
this.addCommand({
id: 'audio-transcript',
name: 'Generate a transcript from the above audio',
editorCallback: (editor: Editor, view: MarkdownView) => {
this.commandGenerateTranscript(editor);
}
});
this.addCommand({
id: 'pdf-prompt',
name: 'Generate text from prompt in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateTextWithPdf(editor, prompt);
};
new PromptModal(this.app, "", onSubmit).open();
}
});
this.addCommand({
id: 'pdf-line',
name: 'Generate text from the current line in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineCotent = editor.getLine(position.line)
this.commandGenerateTextWithPdf(editor, lineCotent);
}
});
this.addCommand({
id: 'pdf-selected',
name: 'Generate text from the selected text in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
this.commandGenerateTextWithPdf(editor, selectedText);
}
});
const extraCommandsForSelected = this.settings.promptsForSelected.split('\n');
for (let command of extraCommandsForSelected) {
command = command.trim();
if (command == null || command == undefined || command.length < 1) continue;
const cid = command.toLowerCase().replace(/ /g, '-');
this.addCommand({
id: cid,
name: command,
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
const prompt = 'You are an assistant who can learn from the text I give to you. Here is the text selected:\n\n' + selectedText + '\n\n' + command;
this.commandGenerateText(editor, prompt);
}
});
}
const extraCommandsForPdf = this.settings.promptsForPdf.split('\n');
for (let command of extraCommandsForPdf) {
command = command.trim();
if (command == null || command == undefined || command.length < 1) continue;
const cid = command.toLowerCase().replace(/ /g, '-');
this.addCommand({
id: cid,
name: command,
editorCallback: (editor: Editor, view: MarkdownView) => {
this.commandGenerateTextWithPdf(editor, command);
}
});
}
// 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: AICommanderPlugin;
constructor(app: App, plugin: AICommanderPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {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('h2', {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();
}));
containerEl.createEl('h2', {text: 'Prompt Perfect'});
new Setting(containerEl)
.setName('Use Prompt Perfect')
.setDesc("Use Prompt Perfect to improve prompts for text and image generation")
.addToggle(value => value
.setValue(this.plugin.settings.usePromptPerfect)
.onChange(async (value) => {
this.plugin.settings.usePromptPerfect = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Prompt Perfect API key')
.setDesc("Find in Prompt Perfect settings")
.addText(text => text
.setPlaceholder('Enter your key')
.setValue(this.plugin.settings.promptPerfectKey)
.onChange(async (value) => {
this.plugin.settings.promptPerfectKey = value;
await this.plugin.saveSettings();
}));
containerEl.createEl('h2', {text: 'Custom Commands'});
containerEl.createEl('p', {text: 'Reload the plugin after changing below settings'});
new Setting(containerEl)
.setName('Custom command for selected text')
.setDesc('Fill in your prompts line by line. They will appear as commands.')
.addTextArea(text => text
.setPlaceholder('Summarise the text\nTranslate into English')
.setValue(this.plugin.settings.promptsForSelected)
.onChange(async (value) => {
this.plugin.settings.promptsForSelected = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Custom command for PDF')
.setDesc('Fill in your prompts line by line. They will appear as commands.')
.addTextArea(text => text
.setPlaceholder('Summarise the PDF')
.setValue(this.plugin.settings.promptsForPdf)
.onChange(async (value) => {
this.plugin.settings.promptsForPdf = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -1,9 +1,9 @@
{
"id": "ai-commander",
"name": "AI Commander",
"version": "1.2.1",
"version": "1.3.4",
"minAppVersion": "1.0.0",
"description": "Generate audio transcripts, images, and text in context of PDF attachments or web search results using OpenAI and Bing API.",
"description": "Generate audio transcripts, images, and text in context of PDF attachments or web search results using OpenAI (ChatGPT) and Bing API.",
"author": "Simon Yang",
"authorUrl": "https://yzh503.github.io",
"fundingUrl": "https://www.buymeacoffee.com/yzh503",

View file

@ -1,44 +0,0 @@
import { App, Modal, Setting } from "obsidian";
export class PromptModal extends Modal {
prompt: string;
onSubmit: (prompt: string) => void;
constructor(
app: App,
defaultprompt: string,
onSubmit: (prompt: string) => void
) {
super(app);
this.prompt = defaultprompt;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h1", { text: "" });
new Setting(contentEl).setName("Generate text from prompt").addText((text) =>
text.setValue(this.prompt).onChange((value) => {
this.prompt = value;
})
);
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Submit")
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.prompt);
})
);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

3767
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{
"name": "obsidian-sample-plugin",
"name": "obsidian-aicommander-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
@ -13,8 +13,6 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@types/stream-buffers": "^3.0.4",
"@types/textract": "^2.4.1",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -24,11 +22,6 @@
"typescript": "4.7.4"
},
"dependencies": {
"axios": "^1.3.4",
"azure-cognitiveservices-websearch": "^2.0.0",
"canvas": "^2.11.0",
"form-data": "^4.0.0",
"ms-rest-azure": "^3.0.2",
"openai": "^3.2.1"
}
}

942
src/main.ts Normal file
View file

@ -0,0 +1,942 @@
import {
App,
Editor,
MarkdownView,
Notice,
Plugin,
PluginSettingTab,
Setting,
loadPdfJs,
requestUrl,
arrayBufferToBase64,
TFolder,
} from 'obsidian';
import { PromptModal } from './modal';
import { Configuration, OpenAIApi, CreateImageRequestSizeEnum } from 'openai';
interface AICommanderPluginSettings {
model: string;
apiKey: string;
imgSize: string;
saveImg: string;
useSearchEngine: boolean;
searchEngine: string;
bingSearchKey: string;
usePromptPerfect: boolean;
promptPerfectKey: string;
promptsForSelected: string;
promptsForPdf: string;
}
const DEFAULT_SETTINGS: AICommanderPluginSettings = {
model: 'gpt-4o',
apiKey: '',
imgSize: '256x256',
saveImg: 'attachment',
useSearchEngine: false,
searchEngine: 'bing',
bingSearchKey: '',
promptPerfectKey: '',
usePromptPerfect: false,
promptsForSelected: '',
promptsForPdf: '',
};
export default class AICommanderPlugin extends Plugin {
settings: AICommanderPluginSettings;
writing: boolean;
async improvePrompt(prompt: string, targetModel: string) {
const data = {
data: {
prompt: prompt,
targetModel: targetModel,
},
};
const params = {
url: 'https://api.promptperfect.jina.ai/optimize',
method: 'POST',
contentType: 'application/json',
body: JSON.stringify(data),
headers: {
'x-api-key': `token ${this.settings.promptPerfectKey}`,
},
};
try {
const response = await requestUrl(params);
const responseJson = response.json;
const errorMessage = responseJson?.error?.message || response.status;
if ('promptOptimized' in responseJson) {
return responseJson.promptOptimized as string;
} else {
throw new Error(errorMessage);
}
} catch(error) {
if (error.message.includes('401')) {
throw new Error('Prompt Perfect API Key is not valid.');
} else {
throw new Error('Prompt Perfect: ' + error.message);
}
}
}
async generateText(prompt: string, editor: Editor, currentLn: number, contextPrompt?: string) {
// Validate prompt and API key
if (!prompt || prompt.trim().length === 0) {
throw new Error('Prompt cannot be empty.');
}
if (!this.settings.apiKey || this.settings.apiKey.trim().length === 0) {
throw new Error('OpenAI API Key is not provided.');
}
let finalPrompt = prompt;
// Optionally improve the prompt
if (this.settings.usePromptPerfect) {
finalPrompt = await this.improvePrompt(prompt, this.settings.model);
}
console.log({finalPrompt})
const messages: any[] = [];
// Add context or search results if needed
if (contextPrompt) {
messages.push({ role: 'system', content: contextPrompt });
} else if (this.settings.useSearchEngine) {
try {
const searchResult = await this.searchText(prompt);
messages.push({
role: 'system',
content:
'As an assistant who can learn information from web search results, your task is to incorporate information from a web search result into your answers when responding to questions. Your response should include the relevant information from your knowledge and the web search result and provide the source markdown URL of the information. Please note that you should be able to handle various types of questions and search queries. Your response should also be clear and concise while incorporating all relevant information from the web search results. Here are the web search result:\n\n' +
JSON.stringify(searchResult),
});
} catch (error) {
console.error('Search failed:', error);
// Proceed without search results if search fails
}
}
messages.push({ role: 'user', content: finalPrompt });
// Prepare request body
const requestBody = {
model: this.settings.model,
messages: messages,
stream: true,
};
let response;
try {
response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.settings.apiKey,
},
});
if (!response.ok) {
const errorResponse = await response.json();
const errorMessage = errorResponse?.error?.message || response.statusText;
throw new Error(`Error from OpenAI API: ${errorMessage}`);
}
} catch (error) {
console.error('API request failed:', error);
throw error;
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body reader available');
}
let lnToWrite = this.getNextNewLine(editor, currentLn);
editor.replaceRange('\n', { line: lnToWrite, ch: 0 });
lnToWrite++;
const cursorPos = { line: lnToWrite, ch: 0 };
let accumulatedText = '';
const decoder = new TextDecoder('utf-8');
let isDone = false;
// Read and process streamed data
while (!isDone) {
const { value, done } = await reader.read();
if (done) break;
accumulatedText += decoder.decode(value, { stream: true });
let parsedIndex;
while ((parsedIndex = accumulatedText.indexOf('\n\n')) !== -1) {
const chunk = accumulatedText.slice(0, parsedIndex).trim();
accumulatedText = accumulatedText.slice(parsedIndex + 2);
if (chunk === 'data: [DONE]') {
isDone = true;
break;
}
if (chunk.startsWith('data:')) {
const jsonStr = chunk.substring('data:'.length).trim();
if (jsonStr) {
let json;
try {
json = JSON.parse(jsonStr);
} catch (e) {
console.error('JSON parse error:', e);
continue;
}
if (json.error) {
throw new Error('Error from OpenAI API: ' + json.error.message);
}
const content = json.choices?.[0]?.delta?.content;
if (content) {
editor.replaceRange(content, cursorPos);
const contentLines = content.split('\n');
if (contentLines.length === 1) {
cursorPos.ch += contentLines[0].length;
} else {
cursorPos.line += contentLines.length - 1;
cursorPos.ch = contentLines[contentLines.length - 1].length;
}
editor.setCursor(cursorPos);
editor.scrollIntoView({ from: cursorPos, to: cursorPos }, true);
}
}
}
}
}
editor.replaceRange('\n', cursorPos);
}
getNextNewLine(editor: Editor, line: number) {
const isLastLine = line === editor.lastLine();
const nextLineContent = editor.getLine(line + 1);
if (isLastLine || (nextLineContent !== undefined && nextLineContent.trim().length > 0)) {
editor.replaceRange('\n', { line, ch: editor.getLine(line).length });
}
return line + 1;
}
writeText(editor: Editor, LnToWrite: number, text: string) {
const newLine = this.getNextNewLine(editor, LnToWrite);
editor.setLine(newLine, text);
return newLine;
}
async getImageBase64(url: string) {
const buffer = await requestUrl(url).arrayBuffer;
return arrayBufferToBase64(buffer);
}
generateRandomString(length: number): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
async generateImage(prompt: string) {
if (prompt.length < 1) throw new Error('Cannot find prompt.');
if (this.settings.apiKey.length <= 1) throw new Error('OpenAI API Key is not provided.');
const configuration = new Configuration({
apiKey: this.settings.apiKey,
});
const openai = new OpenAIApi(configuration);
let newPrompt = prompt;
if (this.settings.usePromptPerfect) {
newPrompt = await this.improvePrompt(prompt, 'dalle');
}
const response = await openai
.createImage({
prompt: newPrompt,
n: 1,
size: this.settings.imgSize as CreateImageRequestSizeEnum,
response_format: 'b64_json',
})
.catch((error) => {
if (error.response) {
throw new Error(`Error. ${error.response.data.error.message}`);
} else if (error.request) {
throw new Error(`No response received!`);
} else {
throw new Error(`Error! ${error.message}`);
}
});
const activeFile = this.app.workspace.getActiveFile();
const size = this.settings.imgSize.split('x')[0];
const filename = activeFile?.basename + '_' + this.generateRandomString(8) + '.png';
const filepath = await this.app.vault.getAvailablePathForAttachments(filename);
console.log(filename);
console.log(filepath);
const base64 = response.data.data[0].b64_json as string;
const buffer = Buffer.from(base64, 'base64');
const fileDir = filepath.split('/');
if (fileDir.length > 1) {
fileDir.pop();
const dirPath = fileDir.join('/');
const exists = this.app.vault.getAbstractFileByPath(dirPath) instanceof TFolder;
if (!exists) await this.app.vault.createFolder(dirPath);
}
await this.app.vault.createBinary(filepath, buffer);
if (this.settings.saveImg == 'attachment') {
return `![${size}](${encodeURI(filepath)})\n`;
} else {
return `![${size}](data:image/png;base64,${response.data.data[0].b64_json})\n`;
}
}
async generateTranscript(audioBuffer: ArrayBuffer, filetype: string) {
if (!this.settings.apiKey || this.settings.apiKey.trim().length === 0) {
throw new Error('OpenAI API Key is not provided.');
}
const mimeTypes: { [key: string]: string } = {
flac: 'audio/flac',
m4a: 'audio/x-m4a',
mp3: 'audio/mpeg',
mp4: 'audio/mp4',
mpeg: 'audio/mpeg',
mpga: 'audio/mpeg',
oga: 'audio/ogg',
ogg: 'audio/ogg',
wav: 'audio/wav',
webm: 'audio/webm',
mov: 'audio/quicktime',
};
const mimeType = mimeTypes[filetype.toLowerCase()] || 'application/octet-stream';
const blob = new Blob([audioBuffer], { type: mimeType });
const formData = new FormData();
formData.append('file', blob, `audio.${filetype}`);
formData.append('model', 'whisper-1');
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + this.settings.apiKey,
},
body: formData,
}).catch((error) => {
console.error('Network error:', error);
if (error.message.includes('401')) {
throw new Error('OpenAI API Key is not valid.');
} else {
throw error;
}
});
if (!response.ok) {
const errorJson = await response.json();
const errorMessage = errorJson?.error?.message || response.statusText;
throw new Error(`${errorMessage}`);
}
const result = await response.json();
if (result && 'text' in result) {
return result.text;
} else {
throw new Error('Error: ' + JSON.stringify(result));
}
}
async searchTextWithoutKey(query: string) {
const params = {
url: 'https://www.bing.com/search?q=' + encodeURIComponent(query),
method: 'GET',
};
const response = await requestUrl(params);
return response.text;
}
async searchTextWithKey(query: string) {
const params = {
url: 'https://api.bing.microsoft.com/v7.0/search?q=' + encodeURIComponent(query),
method: 'GET',
contentType: 'application/json',
headers: {
'Ocp-Apim-Subscription-Key': this.settings.bingSearchKey,
},
};
const response = await requestUrl(params).catch((error) => {
if (error.message.includes('401')) throw new Error('Bing Web Search API Key is not valid.');
else throw error;
});
if ('webPages' in response.json && 'value' in response.json.webPages)
return response.json.webPages.value;
else throw new Error('No web search results: ' + JSON.stringify(response.json));
}
async searchText(query: string) {
if (this.settings.bingSearchKey.length > 1) {
return this.searchTextWithKey(query);
} else {
return this.searchTextWithoutKey(query);
}
}
getCurrentPath() {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) throw new Error('No active file');
const currentPath = activeFile.path.split('/');
currentPath.pop();
const currentPathString = currentPath.join('/');
return currentPathString;
}
async findFilePath(editor: Editor, regexList: RegExp[]): Promise<string> {
const cursorPosition = editor.getCursor();
const text = editor.getValue();
let closestMatch: { index: number; length: number; linkText: string } | null = null;
for (const regex of regexList) {
regex.lastIndex = 0; // Reset regex index
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
const matchStart = match.index;
const matchEnd = regex.lastIndex;
const matchLength = matchEnd - matchStart;
// Check if this match is closer to the cursor
if (
!closestMatch ||
Math.abs(matchStart - editor.posToOffset(cursorPosition)) <
Math.abs(closestMatch.index - editor.posToOffset(cursorPosition))
) {
closestMatch = {
index: matchStart,
length: matchLength,
linkText: match[1] || match[0],
};
}
}
}
if (!closestMatch) {
throw new Error('No file link found near the cursor.');
}
const linkText = closestMatch.linkText;
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) throw new Error('No active file');
const linkedFile = this.app.metadataCache.getFirstLinkpathDest(linkText, activeFile.path);
if (!linkedFile) {
throw new Error(`File "${linkText}" not found in vault.`);
}
// Return the file path
return linkedFile.path;
}
async generateTextWithPdf(prompt: string, editor: Editor, currentLn: number, filepath: string) {
const pdfBuffer = await this.app.vault.adapter.readBinary(filepath);
const pdfjs = await loadPdfJs();
const pdf = await pdfjs.getDocument(pdfBuffer).promise;
const context =
`As an assistant who can learn from text given to you, ` +
`your task is to incorporate information from text given to you into your ` +
`answers when responding to questions. Your response should include the ` +
`relevant information from the text given to you and provide attribution ` +
`by mentioning the page number. Below is the content, ` +
`which is extracted from a PDF file:\n\n`;
let message = context;
for (let i = 0; i < pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const content = await page.getTextContent();
const pageContent = content.items
.map((item: any) => item.str)
.filter((str: string) => str !== '')
.join(' ')
.replace(/\s+/g, ' ');
message += `Page ${i + 1}: ` + pageContent + '\n';
}
return this.generateText(prompt, editor, currentLn, message);
}
commandGenerateText(editor: Editor, prompt: string) {
const currentLn = editor.getCursor('to').line;
if (this.writing) {
new Notice('Generator is already in progress.');
return;
}
this.writing = true;
new Notice('Generating text...');
this.generateText(prompt, editor, currentLn)
.then((text) => {
new Notice('Text completed.');
this.writing = false;
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
this.writing = false;
});
}
commandGenerateTextWithPdf(editor: Editor, prompt: string) {
const currentLn = editor.getCursor('to').line;
const regexList = [
/!\[\[(.+?\.pdf)\]\]/g, // Matches ![[File.pdf]]
/!\[.*?\]\((.+?\.pdf)\)/g, // Matches ![Alt Text](File.pdf)
];
this.findFilePath(editor, regexList)
.then((path) => {
if (this.writing) throw new Error('Generator is already in progress.');
this.writing = true;
new Notice(`Generating text in context of ${path}...`);
this.generateTextWithPdf(prompt, editor, currentLn, path)
.then((text) => {
new Notice('Text completed.');
this.writing = false;
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
this.writing = false;
});
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
});
}
commandGenerateImage(editor: Editor, prompt: string) {
const currentLn = editor.getCursor('to').line;
if (this.writing) {
new Notice('Generator is already in progress.');
return;
}
this.writing = true;
new Notice('Generating image...');
this.generateImage(prompt)
.then((text) => {
this.writeText(editor, currentLn, text);
new Notice('Image Generated.');
this.writing = false;
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
this.writing = false;
});
}
commandGenerateTranscript(editor: Editor) {
const position = editor.getCursor();
const regexList = [
/!\[\[(.+?\.(flac|m4a|mp3|mp4|mpeg|mpga|oga|ogg|wav|webm))\]\]/g, // Matches ![[File.mp3]]
/!\[.*?\]\((.+?\.(flac|m4a|mp3|mp4|mpeg|mpga|oga|ogg|wav|webm))\)/g, // Matches ![Alt Text](File.mp3)
];
this.findFilePath(editor, regexList)
.then((path) => {
const fileType = path.split('.').pop();
if (fileType == undefined || fileType == null || fileType == '') {
new Notice('No audio file found');
} else {
this.app.vault.adapter.exists(path).then((exists) => {
if (!exists) throw new Error(path + ' does not exist');
this.app.vault.adapter.readBinary(path).then((audioBuffer) => {
if (this.writing) {
new Notice('Already in progress.');
return;
}
this.writing = true;
new Notice('Generating transcript...');
this.generateTranscript(audioBuffer, fileType)
.then((result) => {
this.writeText(editor, position.line, result);
new Notice('Transcript Generated.');
this.writing = false;
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
this.writing = false;
});
});
});
}
})
.catch((error) => {
console.log(error.message);
new Notice(error.message);
});
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.prompt-modal-textarea-container {
margin-top: 10px;
margin-bottom: 20px;
}
.prompt-modal-textarea {
width: 100%;
height: 100px;
resize: vertical;
}
.prompt-modal-button-container {
display: flex;
justify-content: flex-end;
}
.prompt-modal-button-container button {
margin-left: 10px;
}
`;
document.head.appendChild(style);
}
async onload() {
await this.loadSettings();
this.writing = false;
this.addStyles();
this.addCommand({
id: 'text-prompt',
name: 'Generate text from prompt',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateText(editor, prompt);
};
new PromptModal(this.app, '', onSubmit).open();
},
});
this.addCommand({
id: 'img-prompt',
name: 'Generate an image from prompt',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateImage(editor, prompt);
};
new PromptModal(this.app, '', onSubmit).open();
},
});
this.addCommand({
id: 'text-line',
name: 'Generate text from the current line',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineContent = editor.getLine(position.line);
this.commandGenerateText(editor, lineContent);
},
});
this.addCommand({
id: 'img-line',
name: 'Generate an image from the current line',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineContent = editor.getLine(position.line);
this.commandGenerateImage(editor, lineContent);
},
});
this.addCommand({
id: 'text-selected',
name: 'Generate text from the selected text',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
this.commandGenerateText(editor, selectedText);
},
});
this.addCommand({
id: 'img-selected',
name: 'Generate an image from the selected text',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
new Notice('Generating image...');
this.commandGenerateImage(editor, selectedText);
},
});
this.addCommand({
id: 'audio-transcript',
name: 'Generate a transcript from the above audio',
editorCallback: (editor: Editor, view: MarkdownView) => {
this.commandGenerateTranscript(editor);
},
});
this.addCommand({
id: 'pdf-prompt',
name: 'Generate text from prompt in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const onSubmit = (prompt: string) => {
this.commandGenerateTextWithPdf(editor, prompt);
};
new PromptModal(this.app, '', onSubmit).open();
},
});
this.addCommand({
id: 'pdf-line',
name: 'Generate text from the current line in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const position = editor.getCursor();
const lineCotent = editor.getLine(position.line);
this.commandGenerateTextWithPdf(editor, lineCotent);
},
});
this.addCommand({
id: 'pdf-selected',
name: 'Generate text from the selected text in context of the above PDF',
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
this.commandGenerateTextWithPdf(editor, selectedText);
},
});
const extraCommandsForSelected = this.settings.promptsForSelected.split('\n');
for (let command of extraCommandsForSelected) {
command = command.trim();
if (command == null || command == undefined || command.length < 1) continue;
const cid = command.toLowerCase().replace(/ /g, '-');
this.addCommand({
id: cid,
name: command,
editorCallback: (editor: Editor, view: MarkdownView) => {
const selectedText = editor.getSelection();
const prompt =
'You are an assistant who can learn from the text I give to you. Here is the text selected:\n\n' +
selectedText +
'\n\n' +
command;
this.commandGenerateText(editor, prompt);
},
});
}
const extraCommandsForPdf = this.settings.promptsForPdf.split('\n');
for (let command of extraCommandsForPdf) {
command = command.trim();
if (command == null || command == undefined || command.length < 1) continue;
const cid = command.toLowerCase().replace(/ /g, '-');
this.addCommand({
id: cid,
name: command,
editorCallback: (editor: Editor, view: MarkdownView) => {
this.commandGenerateTextWithPdf(editor, command);
},
});
}
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new ApiSettingTab(this.app, this));
// 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: AICommanderPlugin;
constructor(app: App, plugin: AICommanderPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { 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('Model')
.setDesc('Select the model to use for content generation')
.addText((text) =>
text
.setPlaceholder('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();
})
);
new Setting(containerEl)
.setName('Image Format')
.setDesc('Select how you want to save the image')
.addDropdown((dropdown) =>
dropdown
.addOption('base64', 'base64')
.addOption('attachment', 'attachment')
.setValue(this.plugin.settings.saveImg)
.onChange(async (value) => {
this.plugin.settings.saveImg = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl('h2', { text: 'Search Engine' });
containerEl.createEl('p', {
text: 'You may use Bing without an API key. Use an API key to achieve the best performance.',
});
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();
})
);
containerEl.createEl('h2', { text: 'Prompt Perfect' });
new Setting(containerEl)
.setName('Use Prompt Perfect')
.setDesc('Use Prompt Perfect to improve prompts for text and image generation')
.addToggle((value) =>
value.setValue(this.plugin.settings.usePromptPerfect).onChange(async (value) => {
this.plugin.settings.usePromptPerfect = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Prompt Perfect API key')
.setDesc('Find in Prompt Perfect settings')
.addText((text) =>
text
.setPlaceholder('Enter your key')
.setValue(this.plugin.settings.promptPerfectKey)
.onChange(async (value) => {
this.plugin.settings.promptPerfectKey = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl('h2', { text: 'Custom Commands' });
containerEl.createEl('p', { text: 'Reload the plugin after changing below settings' });
new Setting(containerEl)
.setName('Custom command for selected text')
.setDesc('Fill in text generator prompts line by line. They will appear as commands.')
.addTextArea((text) =>
text
.setPlaceholder('Summarise the text\nTranslate into English')
.setValue(this.plugin.settings.promptsForSelected)
.onChange(async (value) => {
this.plugin.settings.promptsForSelected = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Custom command for PDF')
.setDesc('Fill in text generator prompts line by line. They will appear as commands.')
.addTextArea((text) =>
text
.setPlaceholder('Summarise the PDF')
.setValue(this.plugin.settings.promptsForPdf)
.onChange(async (value) => {
this.plugin.settings.promptsForPdf = value;
await this.plugin.saveSettings();
})
);
}
}

57
src/modal.ts Normal file
View file

@ -0,0 +1,57 @@
import { App, Modal } from "obsidian";
export class PromptModal extends Modal {
prompt: string;
onSubmit: (prompt: string) => void;
constructor(
app: App,
defaultPrompt: string,
onSubmit: (prompt: string) => void
) {
super(app);
this.prompt = defaultPrompt;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
// Add a title to the modal
contentEl.createEl("h2", { text: "Generate Text from Prompt" });
// Create a container for the textarea
const textareaContainer = contentEl.createDiv({ cls: "prompt-modal-textarea-container" });
// Create the textarea element
const textarea = textareaContainer.createEl("textarea", { cls: "prompt-modal-textarea" });
textarea.value = this.prompt;
textarea.addEventListener("input", () => {
this.prompt = textarea.value;
});
// Create a container for the buttons
const buttonContainer = contentEl.createDiv({ cls: "prompt-modal-button-container" });
// Create the submit button
const submitBtn = buttonContainer.createEl("button", { text: "Submit" });
submitBtn.addClass("mod-cta"); // Obsidian's standard call-to-action button styling
submitBtn.addEventListener("click", () => {
this.close();
this.onSubmit(this.prompt);
});
// Optionally, add a cancel button
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel" });
cancelBtn.addEventListener("click", () => {
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -1,3 +1,3 @@
{
"1.2.1": "1.0.0"
"1.3.4": "1.0.0"
}