18 Commits
1.1.0 ... 1.3.0

Author SHA1 Message Date
Achiya Elyasaf
eb2a9ae867 Raise version 2023-12-04 13:53:23 +02:00
Achiya Elyasaf
a7b540de70 Merge pull request #7 from bThink-BGU/chat-api
Update OpenAI API
2023-12-04 13:52:21 +02:00
Achiya Elyasaf
116c4898d5 Replace the API to the new OpenAI Chat Completion API.
Closes #6.
2023-12-04 13:51:16 +02:00
Achiya Elyasaf
7c475c9de1 First commit for OpenAI's chat api 2023-12-04 12:50:01 +02:00
Achiya Elyasaf
15319c67ef Added an error message when API key is not set. 2023-12-03 09:55:51 +02:00
Achiya Elyasaf
693f3f2bce Fixed a bug in detecting manual changes of keyboard shortcuts 2023-11-19 15:35:15 +02:00
Achiya Elyasaf
a640126c2c Fixed href links in popup window 2023-08-06 10:04:05 +03:00
Achiya Elyasaf
729017fd5f Fixed href links in popup window 2023-08-06 10:02:16 +03:00
Achiya Elyasaf
f92137dcaa Merge remote-tracking branch 'origin/main'
# Conflicts:
#	popup/popup.js
2023-08-06 10:01:43 +03:00
Achiya Elyasaf
a39c3e9edd Fixed href links in popup window 2023-08-06 09:59:35 +03:00
Achiya Elyasaf
b01e48b032 Merge pull request #3 from bThink-BGU/refactor
Improved error handling
2023-08-06 09:51:39 +03:00
Achiya Elyasaf
80039a0342 Update version 2023-08-06 09:48:29 +03:00
Achiya Elyasaf
cc1b0986b9 Update version 2023-08-06 09:36:03 +03:00
achiyae
93b362ca3e CSS games 2023-08-05 23:58:17 +03:00
Achiya Elyasaf
d9a782036f Merge pull request #2 from bThink-BGU/refactor
Refactor
2023-08-05 15:59:02 +03:00
achiyae
60b84e8229 phase 2 2023-08-05 15:57:08 +03:00
achiyae
f0a5f9317f phase 1 2023-08-04 17:57:38 +03:00
Achiya Elyasaf
8377ef4f82 Update privacy 2023-08-02 16:22:23 +03:00
7 changed files with 244 additions and 178 deletions

View File

@@ -69,7 +69,9 @@ Entry 7 & Entry 8 & Entry 9\\
```
## Issues
If you encounter any issues, please open an issue in the project's repository.
If nothing happens when you use the plugin, verify that the plugin's shortcuts are not in conflict with other plugins' shortcuts [here](chrome://extensions/shortcuts).
If you encounter any problem/question, please open an issue in the project's repository.
## Privacy
The plugin does not collect any data. The only data that is sent is the text that you select and the API key that you provide. The data is sent to OpenAI's servers only.
The plugin saves its configuration locally on the users' computer. The plugin sends the API key and the selected text to OpenAI only, and only for the purpose it was made for (i.e., completing and improving text and asking GPT questions). The plugin's authors are not responsible for what OpenAI do with this data. The plugin's authors do not collect any data from the plugin's users.

View File

@@ -42,5 +42,5 @@
"manifest_version": 3,
"name": "LeafLLM",
"homepage_url": "https://github.com/achiyae/LeafLLM",
"version": "1.1.0"
"version": "1.3.0"
}

View File

@@ -8,14 +8,15 @@ body {
.extension-title {
font-size: 24px;
color: #4a4a4a;
margin-bottom: 20px;
margin-bottom: 10px;
}
.form-container {
background-color: #fff;
border-radius: 5px;
margin: 10px 0;
padding: 20px;
margin-bottom: 10px;
padding: 10px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
@@ -38,6 +39,14 @@ body {
margin-bottom: 10px;
}
.btn-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 3px;
margin-right: 3px;
}
.btn {
font-size: 14px;
padding: 8px 16px;
@@ -46,9 +55,9 @@ body {
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px 0;
margin: 5px 5px 0 5px;
transition: background-color 0.3s;
width: 300px;
width: 200px;
}
.btn:hover {
@@ -70,7 +79,6 @@ body {
}
#controls {
margin-top: 30px;
display: flex;
flex-direction: column;
}
@@ -78,7 +86,8 @@ body {
.control {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
margin-bottom: 5px;
margin-top: 5px;
}
.input {
@@ -103,7 +112,7 @@ body {
.settings-text {
font-size: 18px;
color: #4a4a4a;
margin-bottom: 20px;
margin-bottom: 10px;
}
.settings-container {
@@ -115,7 +124,8 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
margin-bottom: 3px;
margin-top: 3px;
}
.settings-item-text {
@@ -126,8 +136,8 @@ body {
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
width: 40px;
height: 24px;
}
.switch input {
@@ -150,8 +160,8 @@ body {
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
@@ -163,11 +173,11 @@ input:checked + .slider {
}
input:checked + .slider:before {
transform: translateX(26px);
transform: translateX(16px);
}
.slider.round {
border-radius: 34px;
border-radius: 24px;
}
.slider.round:before {

View File

@@ -1,22 +1,22 @@
<html>
<head>
<script src="/scripts/jquery.js"></script>
<script src="/popup/popup.js"></script>
<script src="/popup/popup.js" type="module"></script>
<link rel="stylesheet" href="/popup/popup.css"/>
</head>
<body>
<h1 class="extension-title">LeafLLM: an AI-powered Overleaf</h1>
<div id="controls" class="form-container">
<div class="control">
<div class="input"><span>ALT</span><span>I</span></div>
<div class="input" id="shortcut-Improve"><span>ALT</span><span>I</span></div>
<div class="effect">Improve selected text.</div>
</div>
<div class="control">
<div class="input"><span>ALT</span><span>C</span></div>
<div class="input" id="shortcut-Complete"><span>ALT</span><span>C</span></div>
<div class="effect">Complete selected text.</div>
</div>
<div class="control">
<div class="input"><span>ALT</span><span>A</span></div>
<div class="input" id="shortcut-Ask"><span>ALT</span><span>A</span></div>
<div class="effect">Ask GPT.</div>
</div>
</div>
@@ -26,21 +26,21 @@
<div class="settings-item">
<div class="settings-item-text">Text Improvement</div>
<label class="switch">
<input name="text-improvement" class="settings-item-checkbox" type="checkbox" />
<input name="text-Improve" class="settings-item-checkbox" type="checkbox"/>
<span class="slider round"></span>
</label>
</div>
<div class="settings-item">
<div class="settings-item-text">Text Completion</div>
<label class="switch">
<input name="text-completion" class="settings-item-checkbox" type="checkbox" />
<input name="text-Complete" class="settings-item-checkbox" type="checkbox"/>
<span class="slider round"></span>
</label>
</div>
<div class="settings-item">
<div class="settings-item-text">Ask</div>
<label class="switch">
<input name="text-ask" class="settings-item-checkbox" type="checkbox" />
<input name="text-Ask" class="settings-item-checkbox" type="checkbox"/>
<span class="slider round"></span>
</label>
</div>
@@ -49,10 +49,13 @@
<div id="api-token-form" class="form-container">
<div class="api-token-text"><span>API Token is </span><span class="api-token-status">not set</span></div>
<input class="api-token-input" name="api-token" placeholder="OpenAI API Token" type="text"/>
<div class="message">To get an API key, go to <a href="https://platform.openai.com/account/api-keys">https://platform.openai.com/account/api-keys</a> </div>
<div class="message">To get an API key, go to <a href="https://platform.openai.com/account/api-keys">https://platform.openai.com/account/api-keys</a>
</div>
<div class="btn-container">
<button class="submit btn">Set API Token</button>
<button class="clear btn">Reset API Token</button>
</div>
</div>
<div id="message-box" class="message-box"></div>
</body>
</html>

View File

@@ -1,105 +1,114 @@
const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/;
const settings = [
{ key: "textCompletion", name: "text-completion" },
{ key: "textImprovement", name: "text-improvement" },
{ key: "textAsk", name: "text-ask" },
];
const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/
function addMessage(message) {
$("#message-box").append(`<div class="message">${message}</div>`);
$('#message-box').append(`<div class="message">${message}</div>`)
}
function addErrorMessage(message) {
$("#message-box").append(`<div class="error">${message}</div>`);
$('#message-box').append(`<div class="error">${message}</div>`)
}
function clearMessages() {
$("#message-box").empty();
$('#message-box').empty()
}
async function refreshStorage() {
chrome.storage.local.get("openAIAPIKey").then(({ openAIAPIKey }) => {
$("#api-token-form .api-token-status").text(chrome.runtime.lastError || !openAIAPIKey ? "not set" : "set");
});
chrome.storage.local.get('openAIAPIKey').then(({ openAIAPIKey }) => {
$('#api-token-form .api-token-status').text(chrome.runtime.lastError || !openAIAPIKey ? 'not set' : 'set')
})
chrome.storage.local.get(settings.map(({ key }) => key)).then((storage) => {
settings.forEach(({ key, name }) => {
$(`#settings-form input[name='${name}']:checkbox`).prop("checked", storage[key]);
});
});
const commands = await chrome.commands.getAll();
chrome.storage.local.get(['Improve', 'Complete', 'Ask']).then((settings) => {
Object.values(settings).forEach(setting => {
let command = commands.filter(({ name }) => name === setting.key)[0]
if(command.shortcut !== setting.shortcut) {
setting.shortcut = command.shortcut;
if(setting.status === 'enabled' && setting.shortcut === '') {
setting.status = 'error'
}
chrome.storage.local.set({ [setting.key]: setting });
} else if(setting.status === 'enabled' && setting.shortcut === '') {
setting.status = 'error'
chrome.storage.local.set({ [setting.key]: setting })
}
})
let bindingFailures = Object.values(settings)
.filter(({ status }) => status === 'error')
.map(({ key }) => `${key}`)
.join(', ');
if (bindingFailures.length > 0) {
addErrorMessage(`Could not bind the following shortcuts:\n${bindingFailures}.\nYou can set it manually at <a href="chrome://extensions/shortcuts">chrome://extensions/shortcuts</a>.`)
}
Object.values(settings).forEach(({ key, status, shortcut }) => {
$(`#settings-form input[name='text-${key}']:checkbox`).prop('checked', status === 'enabled')
let shortcut2 = shortcut === '' ? 'not set' : shortcut
$(`#shortcut-${key}`).html(`<span>${shortcut2}</span>`)
})
})
}
async function handleAPITokenSet(event) {
event.preventDefault();
event.stopPropagation();
event.preventDefault()
event.stopPropagation()
clearMessages();
clearMessages()
const input = $("#api-token-form").find("input[name='api-token']");
const openAIAPIKey = input.val();
input.val("");
const input = $('#api-token-form').find('input[name=\'api-token\']')
const openAIAPIKey = input.val()
input.val('')
if (!openAIAPIKey || !apiKeyRegex.test(openAIAPIKey)) {
addErrorMessage("Invalid API Token.");
return;
addErrorMessage('Invalid API Token.')
return
}
try {
await chrome.storage.local.set({ openAIAPIKey });
} catch (error) {
console.log(error);
addErrorMessage("Failed to set API Token.");
return;
}
await refreshStorage();
chrome.storage.local.set({ openAIAPIKey })
.then(refreshStorage)
.catch((error) => addErrorMessage(`Failed to remove API Token. Error: ${error}`))
}
async function handleAPITokenClear(event) {
event.preventDefault();
event.stopPropagation();
event.preventDefault()
event.stopPropagation()
clearMessages();
clearMessages()
try {
await chrome.storage.local.remove("openAIAPIKey");
} catch (error) {
console.log(error);
addErrorMessage("Failed to remove API Token.");
return;
}
await refreshStorage();
chrome.storage.local.remove('openAIAPIKey')
.then(refreshStorage)
.catch((error) => addErrorMessage(`Failed to remove API Token. Error: ${error}`))
}
function makeHandleSettingChange(key) {
return async (event) => {
event.preventDefault();
event.stopPropagation();
event.preventDefault()
event.stopPropagation()
clearMessages()
clearMessages();
const value = event.target.checked;
try {
await chrome.storage.local.set({ [key]: value });
} catch (error) {
console.log(error);
addErrorMessage(`Failed to set ${key} setting.`);
return;
const value = event.target.checked
const setting = await chrome.storage.local.get(key)
/* let commandKey = await chrome.commands.getAll()
commandKey = commandKey.filter(({ name }) => name === key)[0]*/
// if (setting[key].status !== 'error') {
setting[key].status = value ? 'enabled' : 'disabled'
await chrome.storage.local.set({ [key]: setting[key] })
// }
return refreshStorage()
}
await refreshStorage();
};
}
$(document).ready(async function () {
$("#api-token-form .submit").on("click", handleAPITokenSet);
$("#api-token-form .clear").on("click", handleAPITokenClear);
$('#api-token-form .submit').on('click', handleAPITokenSet)
$('#api-token-form .clear').on('click', handleAPITokenClear)
settings.forEach(({ key, name }) =>
$(`#settings-form input[name='${name}']:checkbox`).on("change", makeHandleSettingChange(key))
);
await refreshStorage();
let commands = ['Improve', 'Complete', 'Ask']
commands.forEach((key) => {
$(`#settings-form input[name='text-${key}']:checkbox`).on('change', makeHandleSettingChange(key))
})
$('body').on('click', 'a', function(){
chrome.tabs.create({url: $(this).attr('href')});
return false;
});
return refreshStorage()
})

View File

@@ -1,5 +1,6 @@
class OpenAIAPI {
static defaultModel = 'text-davinci-003'
static defaultModel = 'gpt-3.5-turbo'
constructor(apiKey) {
this.apiKey = apiKey
@@ -10,6 +11,8 @@ class OpenAIAPI {
const url = `https://api.openai.com/v1/${endpoint}`
if (!data.model) data.model = OpenAIAPI.defaultModel
if (!data.n) data.n = 1
if (!data.temperature) data.temperature = 0.5
const xhr = new XMLHttpRequest()
xhr.open('POST', url, true)
@@ -33,29 +36,38 @@ class OpenAIAPI {
async completeText(text) {
const data = {
max_tokens: 512,
prompt: text,
n: 1,
temperature: 0.5
messages: [
{ role: 'system', content: 'You are an assistant in a Latex editor' },
{ role: 'user', 'content': text }
],
}
const result = await this.query('completions', data)
return result[0].text
return this.query('chat/completions', data)
.then(result => result[0]['message'].content)
}
async improveText(text) {
const data = {
model: 'code-davinci-edit-001',
input: text,
instruction:
'Correct any spelling mistakes, grammar mistakes, and improve the overall style of the (latex) text.',
n: 1,
temperature: 0.5
messages: [
{ role: 'system', content: 'You are an assistant in a Latex editor' },
{ role: 'user', 'content': 'Improve the following text:\n'+text }],
}
const result = await this.query('edits', data)
return this.query('chat/completions', data)
.then(result => result[0]['message'].content)
}
return result[0].text
async ask(text) {
const data = {
max_tokens: 512,
messages: [
{ role: 'system', content: 'You are an assistant in a Latex editor. Answer questions without introduction/explanations' },
{ role: 'user', 'content': text }
],
}
return this.query('chat/completions', data)
.then(result => result[0]['message'].content)
}
}
@@ -69,14 +81,10 @@ function replaceSelectedText(replacementText, selection) {
}
}
async function settingIsEnabled(setting) {
let result
try {
result = await chrome.storage.local.get(setting)
} catch (error) {
return false
}
return result[setting]
async function settingIsEnabled(key) {
return chrome.storage.local.get(key)
.then(setting => 'enabled' === setting[key].status)
.catch(() => false)
}
function commentText(text) {
@@ -90,7 +98,7 @@ function commentText(text) {
}
async function improveTextHandler(openAI) {
if (!(await settingIsEnabled('textImprovement'))) throw new Error('Text improvement is not enabled.')
if (!(await settingIsEnabled('Improve'))) throw new Error('Text improvement is not enabled.')
const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText) return
@@ -100,20 +108,20 @@ async function improveTextHandler(openAI) {
}
async function completeTextHandler(openAI) {
if (!(await settingIsEnabled('textCompletion'))) throw new Error('Text completion is not enabled.')
if (!(await settingIsEnabled('Complete'))) throw new Error('Text completion is not enabled.')
const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText) return
const editedText = await openAI.completeText(selectedText)
replaceSelectedText(selectedText + editedText, selection)
const editedText = (await openAI.completeText(selectedText)).trimStart()
replaceSelectedText(selectedText + '\n' + editedText, selection)
}
async function askHandler(openAI) {
if (!(await settingIsEnabled('textAsk'))) throw new Error('Text ask is not enabled.')
if (!(await settingIsEnabled('Ask'))) throw new Error('Ask is not enabled.')
const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText) return
const editedText = await openAI.completeText('In Latex, ' + selectedText)
const editedText = (await openAI.ask(selectedText)).trimStart()
replaceSelectedText(editedText, selection)
}
@@ -129,28 +137,38 @@ function setAPIKey(key) {
currentAPIKey = key
if (currentAPIKey) {
openAI = new OpenAIAPI(currentAPIKey)
console.log('LeafLLM: OpenAI API key set, enabling LeafLLM features.')
log('OpenAI API key set, enabling LeafLLM features.')
} else {
openAI = undefined
console.log('LeafLLM: OpenAI API key is not set, LeafLLM features are disabled.')
log('OpenAI API key is not set, LeafLLM features are disabled.')
}
}
function handleCommand(command) {
if (command === 'Improve') {
improveTextHandler(openAI).catch(e => error(`Failed to execute the '${command}' command. Error message: ${e}`))
improveTextHandler(openAI).catch(e => error(`Failed to execute the '${command}' command.`, e))
} else if (command === 'Complete') {
completeTextHandler(openAI).catch(e => error(`Failed to execute the '${command}' command. Error message: ${e}`))
completeTextHandler(openAI).catch(e => error(`Failed to execute the '${command}' command.`, e))
} else if (command === 'Ask') {
askHandler(openAI).catch(e => error(`Failed to execute the '${command}' command. Error message: ${e}`))
askHandler(openAI).catch(e => error(`Failed to execute the '${command}' command.`, e))
}
}
function error(msg) {
function error(msg, error) {
if(error) {
msg += ` Error message: ${error.message}`
if(error.cause) {
console.error(`\nCause: ${JSON.stringify(error.cause)}`)
}
}
customAlert(msg)
console.error(`LeafLLM: ${msg}`)
}
function log(msg) {
console.log(`LeafLLM: ${msg}`)
}
function customAlert(msg,duration)
{
if(!duration) duration = 4000;
@@ -167,7 +185,7 @@ function customAlert(msg,duration)
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
console.log(`Received request: ${JSON.stringify(request)}`)
log(`Received request: ${JSON.stringify(request)}`)
if (request.command === 'setup') {
setAPIKey(request.apiKey)
} else {
@@ -176,6 +194,8 @@ chrome.runtime.onMessage.addListener(
setAPIKey(openAIAPIKey)
if (openAI) {
handleCommand(request.command)
} else {
error('OpenAI API key is not set, LeafLLM features are disabled.')
}
})
} else {

View File

@@ -1,7 +1,13 @@
const settings = [
{ key: 'Complete', shortcut: 'Alt+C', status: 'enabled', type: 'Command' },
{ key: 'Improve', shortcut: 'Alt+I', status: 'enabled', type: 'Command' },
{ key: 'Ask', shortcut: 'Alt+A', status: 'enabled', type: 'Command' }
]
async function sendMessage(message) {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
if (tab == null || tab.url?.startsWith('chrome://')) return undefined
const response = await chrome.tabs.sendMessage(tab.id, message)
chrome.tabs.sendMessage(tab.id, message)
// do something with response here, not outside the function
// console.log(response)
}
@@ -14,19 +20,35 @@ function addListener(commandName) {
})
}
// Only use this function during the initial installation phase. After
// installation the user may have intentionally unassigned commands.
// Example for install commands: [{"description":"","name":"_execute_action","shortcut":""},{"description":"Use the selected text to ask GPT. It adds to the beginning of the selected text: 'In Latex, '","name":"Ask","shortcut":""},{"description":"Complete selected text","name":"Complete","shortcut":""},{"description":"Improve selected text","name":"Improve","shortcut":""}]
async function checkCommandShortcuts() {
chrome.commands.getAll((commands) => {
for (let { name, shortcut } of commands) {
let command =
settings.filter(({ type, key }) => 'Command' === type && name === key)
if (command.length > 0) {
command = command[0]
if (shortcut === '') {
command.status = 'error'
}
chrome.storage.local.set({ [command.key]: command })
}
}
})
}
chrome.runtime.onInstalled.addListener((reason) => {
if (reason.reason === chrome.runtime.OnInstalledReason.INSTALL) {
checkCommandShortcuts()
}
})
async function setup() {
addListener('Improve')
addListener('Complete')
addListener('Ask')
/*const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
if (tab?.url?.startsWith('chrome://')) return undefined
console.log('tab'+tab)
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['scripts/content.js']
}).then(() => {
})*/
}
setup()