ChatAI

name

ChatAI

description

This is similar to OpenAI plugin with the change that it allows for interactive chat with OpenAI. It also supports user defined prompts and proves some pre-defined prompts.

icon

smart_toy

setting

API Key

setting

OpenAI model (e.g., 'gpt-4'. Leave blank for gpt-3.5-turbo)

setting

userPromptName1

setting

userPromptMsg1

setting

userPromptName2

setting

userPromptMsg2

setting

userPromptName3

setting

userPromptMsg3

setting

userPromptName4

setting

userPromptMsg4

setting

userPromptName5

setting

userPromptMsg5

setting

userPromptName6

setting

userPromptMsg6

setting

userPromptName7

setting

userPromptMsg7

setting

userPromptName8

setting

userPromptMsg8

setting

userPromptName9

setting

userPromptMsg9

instructions

Demo:



Instructions:
First, enable the plugin and add your OpenAI API Key in the plugin settings.

After that, there are currently two ways to invoke this plugin:
1. Select a range of text, choose one of the options in the dropdown menu for the rightmost icon


2. Click the triple-dot for a note and choose one of the options that has the AI logo



Tutorials:
ChatAI - User Prompts - Shows how to create user prompts and use them.

--------------------------
This plugin sends requests to the OpenAI endpoint and requires an OpenAI API key. OpenAI gives away free credits for ChatGPT that can last for up to 120 days. To get an OpenAI API key, sign up for OpenAI, then visit OpenAI's API keys page .


Code:

{
constants: {
defaultSystemPrompt: "You are a helpful assistant.",
labelApiKey: "API Key",
labelOpenAiModel: "OpenAI model (e.g., 'gpt-4'. Leave blank for gpt-3.5-turbo)",
requestTimeoutSeconds: 40
},
init: async function(app) {
this.coversationPage.parent = this;
this.selectCoversationPage.parent = this;
this.resultPage.parent = this;
},
replaceText: {
"Complete": async function(app, text) {
await this.init();
const noteHandle = { uuid: app.context.noteUUID };
await this.coversationPage.handleSubmit(app, [
{ role: "context", content: text },
{ role: "user", content: `Please complete ${text.trim().at(-1) != '.' ? 'the sentence provided in' : 'the text provided in'} the context above. Please reply with the resultant text only.` }
], 'replaceText');
},
"Fix grammar": async function(app, text) {
await this.init();
const noteHandle = { uuid: app.context.noteUUID };
await this.coversationPage.handleSubmit(app, [
{ role: "context", content: text },
{ role: "user", content: `Please fix the spelling and grammar provided in context above. Please reply with the resultant text only.` }
], 'replaceText');
},
"User Prompt": async function(app, text) {
await this.init();
const noteHandle = { uuid: app.context.noteUUID };
await this.selectCoversationPage.view(app, [
{ role: "context", content: text },
], 'replaceText');
},
"Chat": async function(app, text) {
await this.init();
const noteHandle = { uuid: app.context.noteUUID };
await this.coversationPage.view(app, [
{ role: "context", content: text },
], 'replaceText');
}
},
noteOption: {
"with page context": async function(app, noteUUID) {
await this.init();
await this.coversationPage.view(app, [
{ role: "context", content: await app.getNoteContent({ uuid: noteUUID }) },
], 'noteOption');
},
"without page context": async function(app) {
await this.init();
await this.coversationPage.view(app, [], 'noteOption');
}
},
resultPage: {
view: async function(app, msgArr, triggeredBy) {
let display = this._getDisplayStrFromMsgArr(msgArr);
let actions = [{ icon: "post_add", label: "Insert bot reply in note" },
{ icon: "chat", label: "Continue conversation" }];
if (triggeredBy === 'replaceText')
actions.splice(1, 0, { icon: "edit", label: "Replace selection with bot reply" });
const actionIndex = await app.alert(display, {
actions
});
if (actionIndex != null)
await this.handleAction(app, msgArr, actions[actionIndex].label, triggeredBy);
},
handleAction: async function(app, msgArr, action, triggeredBy) {
if (action == 'Insert bot reply in note') {
await app.insertNoteContent({ uuid: app.context.noteUUID }, msgArr[msgArr.length - 1].content, { atEnd: true });
}
else if (action == 'Replace selection with bot reply') {
await app.context.replaceSelection(msgArr[msgArr.length - 1].content);
}
else if (action == 'Continue conversation') {
await this.parent.coversationPage.view(app, msgArr, triggeredBy);
}
},
_getDisplayStrFromMsgArr: function(msgArr) {
let display = '';
msgArr.forEach(msg => {
if (msg.role == "system") {
return;
}
else if (msg.role == "context") {
display += "📄"+this.parent._toUnicodeVariant("Context:", "bold")+"\n";
}
else if (msg.role == "user") {
display += "👤"+this.parent._toUnicodeVariant("User:", "bold")+"\n";
}
else if (msg.role == "assistant") {
display += "🤖"+this.parent._toUnicodeVariant("AI:", "bold")+"\n";
}
display += msg.content+"\n\n";
});
return display;
}
},
coversationPage: {
view: async function(app, msgArr, triggeredBy) {
let display = this.parent.resultPage._getDisplayStrFromMsgArr(msgArr);
display += "\nUser:";
let userInput = '';
do {
userInput = await app.prompt(
display,
{inputs: [{label: "", placeholder: "Type your message here.", type: "text"}]}
);
if (userInput == null)
return;
if (userInput != '')
break;
await app.alert("Please enter a message or cancel.");
} while (true);
msgArr.push({ role: "user", content: userInput });
await this.handleSubmit(app, msgArr, triggeredBy);
},
handleSubmit: async function(app, msgArr, triggeredBy) {
let result = await this.parent._callOpenAI(app, msgArr, this.parent);
if (result == null)
return;
msgArr.push({ role: "assistant", content: result });
await this.parent.resultPage.view(app, msgArr, triggeredBy);
}
},
selectCoversationPage: {
view: async function(app, msgArr, triggeredBy) {
let userPrompts = [];
for (let i = 1; i <= 9; i++) {
try {
let name = app.settings[`userPromptName${i}`];
let msg = app.settings[`userPromptMsg${i}`];
if (name != null && name !== "" && msg != null && msg !== "") {
userPrompts.push({label: name, value: msg});
}
} catch (e) {
console.log(e);
}
}
let display = this.parent.resultPage._getDisplayStrFromMsgArr(msgArr);
display += "\nUser:";
let userInput = '';
do {
userInput = await app.prompt(
display,
{inputs: [{label: "", placeholder: "Select prompt:", type: "select", options: userPrompts}]}
);
if (userInput == null)
return;
if (userInput != '')
break;
await app.alert("Please enter a message or cancel.");
} while (true);
msgArr.push({ role: "user", content: userInput });
await this.handleSubmit(app, msgArr, triggeredBy);
},
handleSubmit: async function(app, msgArr, triggeredBy) {
await this.parent.coversationPage.handleSubmit(app, msgArr, triggeredBy);
}
},
// Private Functions
_toUnicodeVariant(str, variant, flags) {
// Source: https://github.com/davidkonrad/toUnicodeVariant/blob/master/toUnicodeVariant.js
const offsets = {
m: [0x1d670, 0x1d7f6],
b: [0x1d400, 0x1d7ce],
i: [0x1d434, 0x00030],
bi: [0x1d468, 0x00030],
c: [0x0001d49c, 0x00030],
bc: [0x1d4d0, 0x00030],
g: [0x1d504, 0x00030],
d: [0x1d538, 0x1d7d8],
bg: [0x1d56c, 0x00030],
s: [0x1d5a0, 0x1d7e2],
bs: [0x1d5d4, 0x1d7ec],
is: [0x1d608, 0x00030],
bis: [0x1d63c, 0x00030],
o: [0x24B6, 0x2460],
on: [0x0001f150, 0x2460],
p: [0x249c, 0x2474],
q: [0x1f130, 0x00030],
qn: [0x0001F170, 0x00030],
w: [0xff21, 0xff10],
u: [0x2090, 0xff10]
}
 
const variantOffsets = {
'monospace': 'm',
'bold' : 'b',
'italic' : 'i',
'bold italic' : 'bi',
'script': 'c',
'bold script': 'bc',
'gothic': 'g',
'gothic bold': 'bg',
'doublestruck': 'd',
'sans': 's',
'bold sans' : 'bs',
'italic sans': 'is',
'bold italic sans': 'bis',
'parenthesis': 'p',
'circled': 'o',
'circled negative': 'on',
'squared': 'q',
'squared negative': 'qn',
'fullwidth': 'w'
}
 
// special characters (absolute values)
const special = {
m: {
' ': 0x2000,
'-': 0x2013
},
i: {
'h': 0x210e
},
g: {
'C': 0x212d,
'H': 0x210c,
'I': 0x2111,
'R': 0x211c,
'Z': 0x2128
},
d: {
'C': 0x2102,
'H': 0x210D,
'N': 0x2115,
'P': 0x2119,
'Q': 0x211A,
'R': 0x211D,
'Z': 0x2124
},
o: {
'0': 0x24EA,
'1': 0x2460,
'2': 0x2461,
'3': 0x2462,
'4': 0x2463,
'5': 0x2464,
'6': 0x2465,
'7': 0x2466,
'8': 0x2467,
'9': 0x2468,
},
on: {},
p: {},
q: {},
qn: {},
w: {}
}
//support for parenthesized latin letters small cases
//support for full width latin letters small cases
//support for circled negative letters small cases
//support for squared letters small cases
//support for squared letters negative small cases
;['p', 'w', 'on', 'q', 'qn'].forEach(t => {
for (var i = 97; i <= 122; i++) {
special[t][String.fromCharCode(i)] = offsets[t][0] + (i-97)
}
})
 
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
const numbers = '0123456789'
 
const getType = function(variant) {
if (variantOffsets[variant]) return variantOffsets[variant]
if (offsets[variant]) return variant
return 'm' //monospace as default
}
const getFlag = function(flag, flags) {
if (!flags) return false
return flag.split('|').some(f => flags.split(',').indexOf(f) > -1)
}
 
const type = getType(variant)
const underline = getFlag('underline|u', flags)
const strike = getFlag('strike|s', flags)
let result = ''
 
for (let c of str) {
let index
if (special[type] && special[type][c]) c = String.fromCodePoint(special[type][c])
if (type && (index = chars.indexOf(c)) > -1) {
result += String.fromCodePoint(index + offsets[type][0])
} else if (type && (index = numbers.indexOf(c)) > -1) {
result += String.fromCodePoint(index + offsets[type][1])
} else {
result += c
}
if (underline) result += '\u0332' // add combining underline
if (strike) result += '\u0336' // add combining strike
}
return result
},
async _fetch_openAI_requestWithRetry(app, model, messages, apiKey, { retries = 3, timeoutSeconds = 30 } = {}) {
let error, response;
 
for (let i = 0; i < retries; i++) {
try {
response = await Promise.race([
fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${ apiKey }`,
"Content-Type": "application/json"
},
body: JSON.stringify({ model, messages })
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutSeconds * 1000)
)
]);
break;
} catch (e) {
error = e;
console.log(`Attempt ${ i + 1 } failed with`, e, `at ${ new Date() }. Retrying...`);
}
}
 
if (!response) {
app.alert("Failed to call OpenAI: " + error);
return null;
} else if (response.ok) {
const result = await response.json();
 
const { choices: [ { message: { content } } ] } = result;
return content;
} else if (response.status === 401) {
app.alert("Invalid OpenAI key. Please configure your OpenAI key properly in plugin settings.");
return null;
} else {
const result = await response.json();
if (result && result.error) {
app.alert("Failed to call OpenAI: " + result.error.message);
return null;
}
}
},
async _callOpenAI(app, messages, plugin) {
let model = app.settings[plugin.constants.labelOpenAiModel];
model = model?.trim()?.length ? model : "gpt-3.5-turbo";
let apiKey = app.settings[plugin.constants.labelApiKey];
if (!apiKey) {
app.alert("Please configure your OpenAI key in plugin settings.");
return null;
}
apiKey = apiKey.trim();
let messagesClone = JSON.parse(JSON.stringify(messages));
messagesClone.forEach(msg => {
if (msg.role == "context") {
msg.role = "user";
msg.content = "Context: \n" + msg.content;
}
});
try {
return await this._fetch_openAI_requestWithRetry(app, model, messagesClone, apiKey,
{ timeoutSeconds: plugin.constants.requestTimeoutSeconds });
} catch (error) {
app.alert("Failed to call OpenAI: " + error);
return null;
}
}
}