mirror of
https://github.com/yzh503/obsidian-aicommander-plugin.git
synced 2026-07-22 07:40:26 +00:00
Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4803788959 | ||
|
|
60c3f51d2c | ||
|
|
330ce44d20 | ||
|
|
e2b46a8f2e | ||
|
|
a6fb4afbb6 | ||
|
|
6750ad7062 | ||
|
|
e6af43e076 | ||
|
|
71edf59e51 | ||
|
|
174cb2fc47 | ||
|
|
2fa22c6c88 | ||
|
|
64b6f35f55 | ||
|
|
7a5f9c14a4 | ||
|
|
8eebc1c572 | ||
|
|
b6bbb83b47 | ||
|
|
3e80d04c40 | ||
|
|
85e2da4f52 | ||
|
|
a2454d06ac | ||
|
|
d951df8e3d | ||
|
|
0e64454235 | ||
|
|
0e6911c8ff | ||
|
|
ae1a176893 | ||
|
|
c7248e6957 | ||
|
|
5fcd2ec676 | ||
|
|
85fff866fa | ||
|
|
aa6d48b68c | ||
|
|
592f819a47 |
12 changed files with 1276 additions and 4298 deletions
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100
|
||||
}
|
||||
10
README.md
10
README.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
[OpenAI API key](https://platform.openai.com/account/api-keys) is required to use this plugin.
|
||||
|
||||
|
|
@ -18,9 +20,9 @@
|
|||
|
||||
## 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
BIN
demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
|
|
@ -15,7 +15,7 @@ const context = await esbuild.context({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
|
|
|
|||
731
main.ts
731
main.ts
|
|
@ -1,731 +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;
|
||||
saveImg: 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',
|
||||
saveImg: 'attachment',
|
||||
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: 'system',
|
||||
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: 'system',
|
||||
content: 'As an assistant who can learn information from web search results, your task is to incorporate information from a web search API JSON response into your answers when responding to questions. Your response should include the relevant information from the JSON and provide attribution by mentioning the source of information with its url in the format of markdown. 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
generateRandomString(length: number): string {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
const size = this.settings.imgSize.split('x')[0];
|
||||
|
||||
const currentPathString = this.getCurrentPath();
|
||||
|
||||
const filepath = await this.getAttachmentDir().then((attachmentPath) => {
|
||||
let dir = ''
|
||||
if (attachmentPath == '' || attachmentPath == '/') dir = '';
|
||||
else if (attachmentPath.startsWith('./')) dir = currentPathString + '/' + attachmentPath.substring(2);
|
||||
else dir = attachmentPath;
|
||||
|
||||
const path = dir.trim() + '/' + this.generateRandomString(20) + '.png';
|
||||
return path.replace(/\/\//g, '/');
|
||||
});
|
||||
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('/');
|
||||
await this.app.vault.adapter.exists(dirPath).then((exists) => {
|
||||
if (exists) return;
|
||||
return this.app.vault.adapter.mkdir(dirPath);
|
||||
});
|
||||
}
|
||||
|
||||
await this.app.vault.adapter.writeBinary(filepath, buffer);
|
||||
|
||||
console.log(this.settings.saveImg);
|
||||
|
||||
if (this.settings.saveImg == 'attachment') {
|
||||
return({
|
||||
prompt: prompt,
|
||||
text: `})\n`
|
||||
});
|
||||
} else {
|
||||
return({
|
||||
prompt: prompt,
|
||||
text: `\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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 currentPathString = this.getCurrentPath();
|
||||
|
||||
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();
|
||||
}));
|
||||
|
||||
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'});
|
||||
|
||||
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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "ai-commander",
|
||||
"name": "AI Commander",
|
||||
"version": "1.2.2",
|
||||
"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",
|
||||
|
|
|
|||
44
modal.ts
44
modal.ts
|
|
@ -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
3767
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
942
src/main.ts
Normal 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 `})\n`;
|
||||
} else {
|
||||
return `\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 
|
||||
];
|
||||
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 
|
||||
];
|
||||
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
57
src/modal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.2.2": "1.0.0"
|
||||
"1.3.4": "1.0.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue