5 Commits
1.3.0 ... 1.4.0

Author SHA1 Message Date
Achiya Elyasaf
f054228c6b Added explanation to the new configuration to the README.md 2023-12-06 15:50:59 +02:00
Achiya Elyasaf
def21b1450 Added the ability to change the config for each command.
Closes #1
2023-12-06 15:47:12 +02:00
Achiya Elyasaf
8fe42a658a Improved error handling 2023-12-06 08:33:41 +02:00
Achiya Elyasaf
082afd778f Raise version 2023-12-05 11:00:37 +02:00
Achiya Elyasaf
dd19cd9fa3 Fixed the system context for the Complete command 2023-12-05 10:59:37 +02:00
8 changed files with 254 additions and 74 deletions

View File

@@ -17,6 +17,41 @@ Just go to the [extension's page](https://chrome.google.com/webstore/detail/leaf
## Configuration ## Configuration
The plugin can be configured by clicking the plugin button in the Chrome toolbar. It requires inserting an API key from [OpenAI](https://platform.openai.com/account/api-keys). You also need to choose which tools you wish to enable. The plugin can be configured by clicking the plugin button in the Chrome toolbar. It requires inserting an API key from [OpenAI](https://platform.openai.com/account/api-keys). You also need to choose which tools you wish to enable.
If you feel advanced, you can also change the request JSON sent to OpenAI and also the base URL.
To do that, go to the 'Advance Configuration' component in the configuration page. By default, the value is
```json
{
openai: {
url: 'https://api.openai.com/v1/chat/completions',
base: {
n: 1,
temperature: 0.5,
model: 'gpt-3.5-turbo'
},
Complete: {
max_tokens: 512,
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that continues the given text. No need to rewrite the given text'
}]
},
Improve: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that improves the given text'
}]
},
Ask: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor. Answer questions without introduction/explanations'
}]
}
}
}
```
Base is the default configuration which is overridden by the specific command configuration.
## Usage ## Usage
These are the tools that are currently available: These are the tools that are currently available:

View File

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

6
popup/jsoneditor.min.css vendored Normal file

File diff suppressed because one or more lines are too long

46
popup/jsoneditor.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,10 @@
<html> <html>
<head> <head>
<script src="/scripts/jquery.js"></script> <script src="/scripts/jquery.js"></script>
<script src="/popup/jsoneditor.min.js"></script>
<script src="/popup/popup.js" type="module"></script> <script src="/popup/popup.js" type="module"></script>
<link rel="stylesheet" href="/popup/popup.css"/> <link rel="stylesheet" href="/popup/popup.css"/>
<link rel="stylesheet" href="/popup/jsoneditor.min.css"/>
</head> </head>
<body> <body>
<h1 class="extension-title">LeafLLM: an AI-powered Overleaf</h1> <h1 class="extension-title">LeafLLM: an AI-powered Overleaf</h1>
@@ -57,5 +59,14 @@
</div> </div>
</div> </div>
<div id="message-box" class="message-box"></div> <div id="message-box" class="message-box"></div>
<div id="configuration-form" class="form-container">
<div class="settings-text">Advanced Configuration</div>
<!-- JSON Editor Container -->
<div id="json-editor" style="height: 300px;"></div>
<div class="btn-container">
<button class="submit btn" id="saveConfig">Save config</button>
<button class="clear btn" id="resetConfig">Reset config</button>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,45 @@
// Changing defaultConfigurations requires changing service-worker.js
const defaultConfigurations = {
openai: {
url: 'https://api.openai.com/v1/chat/completions',
base: {
n: 1,
temperature: 0.5,
model: 'gpt-3.5-turbo'
},
Complete: {
max_tokens: 512,
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that continues the given text. No need to rewrite the given text'
}]
},
Improve: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that improves the given text'
}]
},
Ask: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor. Answer questions without introduction/explanations'
}]
}
}
}
const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/ const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/
const jsonEditor = createJsonEditor()
function createJsonEditor() {
return new JSONEditor($('#json-editor')[0], {
mode: 'code', // Use code mode for better editing
onChange: function () {
$('#saveConfig').prop('disabled', false);
}
})
}
function addMessage(message) { function addMessage(message) {
$('#message-box').append(`<div class="message">${message}</div>`) $('#message-box').append(`<div class="message">${message}</div>`)
@@ -17,18 +58,23 @@ async function refreshStorage() {
$('#api-token-form .api-token-status').text(chrome.runtime.lastError || !openAIAPIKey ? 'not set' : 'set') $('#api-token-form .api-token-status').text(chrome.runtime.lastError || !openAIAPIKey ? 'not set' : 'set')
}) })
const commands = await chrome.commands.getAll(); const commands = await chrome.commands.getAll()
chrome.storage.local.get(['RequestConfiguration']).then((settings) => {
jsonEditor.set(settings.RequestConfiguration)
$('#saveConfig').prop('disabled', true);
})
chrome.storage.local.get(['Improve', 'Complete', 'Ask']).then((settings) => { chrome.storage.local.get(['Improve', 'Complete', 'Ask']).then((settings) => {
Object.values(settings).forEach(setting => { Object.values(settings).forEach(setting => {
let command = commands.filter(({ name }) => name === setting.key)[0] let command = commands.filter(({ name }) => name === setting.key)[0]
if(command.shortcut !== setting.shortcut) { if (command.shortcut !== setting.shortcut) {
setting.shortcut = command.shortcut; setting.shortcut = command.shortcut
if(setting.status === 'enabled' && setting.shortcut === '') { if (setting.status === 'enabled' && setting.shortcut === '') {
setting.status = 'error' setting.status = 'error'
} }
chrome.storage.local.set({ [setting.key]: setting }); chrome.storage.local.set({ [setting.key]: setting })
} else if(setting.status === 'enabled' && setting.shortcut === '') { } else if (setting.status === 'enabled' && setting.shortcut === '') {
setting.status = 'error' setting.status = 'error'
chrome.storage.local.set({ [setting.key]: setting }) chrome.storage.local.set({ [setting.key]: setting })
} }
@@ -36,7 +82,7 @@ async function refreshStorage() {
let bindingFailures = Object.values(settings) let bindingFailures = Object.values(settings)
.filter(({ status }) => status === 'error') .filter(({ status }) => status === 'error')
.map(({ key }) => `${key}`) .map(({ key }) => `${key}`)
.join(', '); .join(', ')
if (bindingFailures.length > 0) { 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>.`) addErrorMessage(`Could not bind the following shortcuts:\n${bindingFailures}.\nYou can set it manually at <a href="chrome://extensions/shortcuts">chrome://extensions/shortcuts</a>.`)
} }
@@ -87,7 +133,7 @@ function makeHandleSettingChange(key) {
const value = event.target.checked const value = event.target.checked
const setting = await chrome.storage.local.get(key) const setting = await chrome.storage.local.get(key)
/* let commandKey = await chrome.commands.getAll() /* let commandKey = await chrome.commands.getAll()
commandKey = commandKey.filter(({ name }) => name === key)[0]*/ commandKey = commandKey.filter(({ name }) => name === key)[0]*/
// if (setting[key].status !== 'error') { // if (setting[key].status !== 'error') {
setting[key].status = value ? 'enabled' : 'disabled' setting[key].status = value ? 'enabled' : 'disabled'
@@ -97,6 +143,20 @@ function makeHandleSettingChange(key) {
} }
} }
async function saveConfig() {
clearMessages()
let config;
try {
config = jsonEditor.get()
} catch (e) {
addErrorMessage(`Failed to parse configuration. Error: ${e}`)
return
}
chrome.storage.local.set({ RequestConfiguration: config })
.then(refreshStorage)
.catch((error) => addErrorMessage(`Failed to save configuration. Error: ${error}`))
}
$(document).ready(async function () { $(document).ready(async function () {
$('#api-token-form .submit').on('click', handleAPITokenSet) $('#api-token-form .submit').on('click', handleAPITokenSet)
$('#api-token-form .clear').on('click', handleAPITokenClear) $('#api-token-form .clear').on('click', handleAPITokenClear)
@@ -106,9 +166,16 @@ $(document).ready(async function () {
$(`#settings-form input[name='text-${key}']:checkbox`).on('change', makeHandleSettingChange(key)) $(`#settings-form input[name='text-${key}']:checkbox`).on('change', makeHandleSettingChange(key))
}) })
$('body').on('click', 'a', function(){ $('body').on('click', 'a', function () {
chrome.tabs.create({url: $(this).attr('href')}); chrome.tabs.create({ url: $(this).attr('href') })
return false; return false
}); })
$('#resetConfig').on('click', async function () {
jsonEditor.set(defaultConfigurations)
await saveConfig()
})
$('#saveConfig').on('click', saveConfig)
return refreshStorage() return refreshStorage()
}) })

View File

@@ -6,67 +6,45 @@ class OpenAIAPI {
this.apiKey = apiKey this.apiKey = apiKey
} }
query(endpoint, data) { query(url, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
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() const xhr = new XMLHttpRequest()
xhr.open('POST', url, true) xhr.open('POST', url, true)
xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Authorization', `Bearer ${this.apiKey}`) xhr.setRequestHeader('Authorization', `Bearer ${this.apiKey}`)
xhr.onreadystatechange = function () { xhr.onerror = function () {
if (xhr.readyState !== 4) return reject('Failed to query OpenAI API: network error.')
if (xhr.status !== 200) return reject('Failed to query OpenAI API.') }
xhr.onload = function () {
const jsonResponse = JSON.parse(xhr.responseText) if (xhr.status === 200) {
let jsonResponse
if (!jsonResponse.choices) return reject('Failed to query OpenAI API.') try {
jsonResponse = JSON.parse(xhr.responseText)
return resolve(jsonResponse.choices) } catch (e) {
reject('Failed to query OpenAI API, cannot parse response:\n' + e + '\n' + xhr.responseText)
return
}
if (jsonResponse.hasOwnProperty('choices')) {
resolve(jsonResponse.choices)
} else {
reject('Failed to query OpenAI API: invalid response: ' + jsonResponse)
}
} else {
reject('Failed to query OpenAI API: invalid status: ' + xhr.status + ' - ' + xhr.responseText)
}
} }
xhr.send(JSON.stringify(data)) xhr.send(JSON.stringify(data))
}) })
} }
async completeText(text) { async act(command, text) {
const data = { let conf = (await chrome.storage.local.get('RequestConfiguration')).RequestConfiguration.openai
max_tokens: 512, let request = Object.assign({}, conf.base)
messages: [ let url = conf.url
{ role: 'system', content: 'You are an assistant in a Latex editor' }, Object.assign(request, conf[command])
{ role: 'user', 'content': text } request.messages.push({ role: 'user', 'content': text })
], return this.query(url, request)
}
return this.query('chat/completions', data)
.then(result => result[0]['message'].content)
}
async improveText(text) {
const data = {
messages: [
{ role: 'system', content: 'You are an assistant in a Latex editor' },
{ role: 'user', 'content': 'Improve the following text:\n'+text }],
}
return this.query('chat/completions', data)
.then(result => result[0]['message'].content)
}
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) .then(result => result[0]['message'].content)
} }
} }
@@ -102,7 +80,7 @@ async function improveTextHandler(openAI) {
const selection = window.getSelection() const selection = window.getSelection()
const selectedText = selection.toString() const selectedText = selection.toString()
if (!selectedText) return if (!selectedText) return
const editedText = await openAI.improveText(selectedText) const editedText = await openAI.act('Improve', selectedText)
const commentedText = commentText(selectedText) const commentedText = commentText(selectedText)
replaceSelectedText(commentedText + '\n' + editedText, selection) replaceSelectedText(commentedText + '\n' + editedText, selection)
} }
@@ -112,7 +90,7 @@ async function completeTextHandler(openAI) {
const selection = window.getSelection() const selection = window.getSelection()
const selectedText = selection.toString() const selectedText = selection.toString()
if (!selectedText) return if (!selectedText) return
const editedText = (await openAI.completeText(selectedText)).trimStart() const editedText = (await openAI.act('Complete', selectedText)).trimStart()
replaceSelectedText(selectedText + '\n' + editedText, selection) replaceSelectedText(selectedText + '\n' + editedText, selection)
} }
@@ -121,7 +99,7 @@ async function askHandler(openAI) {
const selection = window.getSelection() const selection = window.getSelection()
const selectedText = selection.toString() const selectedText = selection.toString()
if (!selectedText) return if (!selectedText) return
const editedText = (await openAI.ask(selectedText)).trimStart() const editedText = (await openAI.act('Ask', selectedText)).trimStart()
replaceSelectedText(editedText, selection) replaceSelectedText(editedText, selection)
} }
@@ -156,10 +134,7 @@ function handleCommand(command) {
function error(msg, error) { function error(msg, error) {
if(error) { if(error) {
msg += ` Error message: ${error.message}` msg += ` Error message: ${JSON.stringify(error)}`
if(error.cause) {
console.error(`\nCause: ${JSON.stringify(error.cause)}`)
}
} }
customAlert(msg) customAlert(msg)
console.error(`LeafLLM: ${msg}`) console.error(`LeafLLM: ${msg}`)

View File

@@ -1,7 +1,39 @@
// Changing defaultConfigurations requires changing popup/popup.js
const defaultConfigurations = {
openai: {
url: 'https://api.openai.com/v1/chat/completions',
base: {
n: 1,
temperature: 0.5,
model: 'gpt-3.5-turbo'
},
Complete: {
max_tokens: 512,
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that continues the given text. No need to rewrite the given text'
}]
},
Improve: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor that improves the given text'
}]
},
Ask: {
messages: [{
role: 'system',
content: 'You are an assistant in a Latex editor. Answer questions without introduction/explanations'
}]
}
}
}
const settings = [ const settings = [
{ key: 'Complete', shortcut: 'Alt+C', status: 'enabled', type: 'Command' }, { key: 'Complete', shortcut: 'Alt+C', status: 'enabled', type: 'Command' },
{ key: 'Improve', shortcut: 'Alt+I', status: 'enabled', type: 'Command' }, { key: 'Improve', shortcut: 'Alt+I', status: 'enabled', type: 'Command' },
{ key: 'Ask', shortcut: 'Alt+A', status: 'enabled', type: 'Command' } { key: 'Ask', shortcut: 'Alt+A', status: 'enabled', type: 'Command' },
{ key: 'RequestConfiguration', value: defaultConfigurations, type: 'Configuration' }
] ]
async function sendMessage(message) { async function sendMessage(message) {
@@ -42,6 +74,14 @@ async function checkCommandShortcuts() {
chrome.runtime.onInstalled.addListener((reason) => { chrome.runtime.onInstalled.addListener((reason) => {
if (reason.reason === chrome.runtime.OnInstalledReason.INSTALL) { if (reason.reason === chrome.runtime.OnInstalledReason.INSTALL) {
checkCommandShortcuts() checkCommandShortcuts()
chrome.storage.local.set({ RequestConfiguration: defaultConfigurations })
} else if (reason.reason === chrome.runtime.OnInstalledReason.UPDATE) {
let newVersion = chrome.runtime.getManifest().version
let oldVersion = reason.previousVersion
let oldVersionArray = oldVersion.split('.')
if (parseInt(oldVersionArray[0]) === 1 && parseInt(oldVersionArray[1]) < 4) {
chrome.storage.local.set({ RequestConfiguration: defaultConfigurations })
}
} }
}) })