Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f054228c6b | ||
|
|
def21b1450 | ||
|
|
8fe42a658a |
35
README.md
35
README.md
@@ -17,6 +17,41 @@ Just go to the [extension's page](https://chrome.google.com/webstore/detail/leaf
|
||||
## 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.
|
||||
|
||||
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
|
||||
These are the tools that are currently available:
|
||||
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"manifest_version": 3,
|
||||
"name": "LeafLLM",
|
||||
"homepage_url": "https://github.com/achiyae/LeafLLM",
|
||||
"version": "1.3.1"
|
||||
"version": "1.4.0"
|
||||
}
|
||||
|
||||
6
popup/jsoneditor.min.css
vendored
Normal file
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
46
popup/jsoneditor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,8 +1,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/scripts/jquery.js"></script>
|
||||
<script src="/popup/jsoneditor.min.js"></script>
|
||||
<script src="/popup/popup.js" type="module"></script>
|
||||
<link rel="stylesheet" href="/popup/popup.css"/>
|
||||
<link rel="stylesheet" href="/popup/jsoneditor.min.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="extension-title">LeafLLM: an AI-powered Overleaf</h1>
|
||||
@@ -57,5 +59,14 @@
|
||||
</div>
|
||||
</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>
|
||||
</html>
|
||||
|
||||
@@ -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 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) {
|
||||
$('#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')
|
||||
})
|
||||
|
||||
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) => {
|
||||
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 === '') {
|
||||
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 === '') {
|
||||
chrome.storage.local.set({ [setting.key]: setting })
|
||||
} else if (setting.status === 'enabled' && setting.shortcut === '') {
|
||||
setting.status = 'error'
|
||||
chrome.storage.local.set({ [setting.key]: setting })
|
||||
}
|
||||
@@ -36,7 +82,7 @@ async function refreshStorage() {
|
||||
let bindingFailures = Object.values(settings)
|
||||
.filter(({ status }) => status === 'error')
|
||||
.map(({ key }) => `${key}`)
|
||||
.join(', ');
|
||||
.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>.`)
|
||||
}
|
||||
@@ -87,7 +133,7 @@ function makeHandleSettingChange(key) {
|
||||
|
||||
const value = event.target.checked
|
||||
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]*/
|
||||
// if (setting[key].status !== 'error') {
|
||||
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 () {
|
||||
$('#api-token-form .submit').on('click', handleAPITokenSet)
|
||||
$('#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))
|
||||
})
|
||||
|
||||
$('body').on('click', 'a', function(){
|
||||
chrome.tabs.create({url: $(this).attr('href')});
|
||||
return false;
|
||||
});
|
||||
$('body').on('click', 'a', function () {
|
||||
chrome.tabs.create({ url: $(this).attr('href') })
|
||||
return false
|
||||
})
|
||||
|
||||
$('#resetConfig').on('click', async function () {
|
||||
jsonEditor.set(defaultConfigurations)
|
||||
await saveConfig()
|
||||
})
|
||||
|
||||
$('#saveConfig').on('click', saveConfig)
|
||||
return refreshStorage()
|
||||
})
|
||||
|
||||
@@ -6,67 +6,45 @@ class OpenAIAPI {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
query(endpoint, data) {
|
||||
query(url, data) {
|
||||
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()
|
||||
xhr.open('POST', url, true)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${this.apiKey}`)
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState !== 4) return
|
||||
if (xhr.status !== 200) return reject('Failed to query OpenAI API.')
|
||||
|
||||
const jsonResponse = JSON.parse(xhr.responseText)
|
||||
|
||||
if (!jsonResponse.choices) return reject('Failed to query OpenAI API.')
|
||||
|
||||
return resolve(jsonResponse.choices)
|
||||
xhr.onerror = function () {
|
||||
reject('Failed to query OpenAI API: network error.')
|
||||
}
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 200) {
|
||||
let jsonResponse
|
||||
try {
|
||||
jsonResponse = JSON.parse(xhr.responseText)
|
||||
} 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))
|
||||
})
|
||||
}
|
||||
|
||||
async completeText(text) {
|
||||
const data = {
|
||||
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' },
|
||||
{ role: 'user', 'content': text }
|
||||
],
|
||||
}
|
||||
|
||||
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)
|
||||
async act(command, text) {
|
||||
let conf = (await chrome.storage.local.get('RequestConfiguration')).RequestConfiguration.openai
|
||||
let request = Object.assign({}, conf.base)
|
||||
let url = conf.url
|
||||
Object.assign(request, conf[command])
|
||||
request.messages.push({ role: 'user', 'content': text })
|
||||
return this.query(url, request)
|
||||
.then(result => result[0]['message'].content)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +80,7 @@ async function improveTextHandler(openAI) {
|
||||
const selection = window.getSelection()
|
||||
const selectedText = selection.toString()
|
||||
if (!selectedText) return
|
||||
const editedText = await openAI.improveText(selectedText)
|
||||
const editedText = await openAI.act('Improve', selectedText)
|
||||
const commentedText = commentText(selectedText)
|
||||
replaceSelectedText(commentedText + '\n' + editedText, selection)
|
||||
}
|
||||
@@ -112,7 +90,7 @@ async function completeTextHandler(openAI) {
|
||||
const selection = window.getSelection()
|
||||
const selectedText = selection.toString()
|
||||
if (!selectedText) return
|
||||
const editedText = (await openAI.completeText(selectedText)).trimStart()
|
||||
const editedText = (await openAI.act('Complete', selectedText)).trimStart()
|
||||
replaceSelectedText(selectedText + '\n' + editedText, selection)
|
||||
}
|
||||
|
||||
@@ -121,7 +99,7 @@ async function askHandler(openAI) {
|
||||
const selection = window.getSelection()
|
||||
const selectedText = selection.toString()
|
||||
if (!selectedText) return
|
||||
const editedText = (await openAI.ask(selectedText)).trimStart()
|
||||
const editedText = (await openAI.act('Ask', selectedText)).trimStart()
|
||||
replaceSelectedText(editedText, selection)
|
||||
}
|
||||
|
||||
@@ -156,10 +134,7 @@ function handleCommand(command) {
|
||||
|
||||
function error(msg, error) {
|
||||
if(error) {
|
||||
msg += ` Error message: ${error.message}`
|
||||
if(error.cause) {
|
||||
console.error(`\nCause: ${JSON.stringify(error.cause)}`)
|
||||
}
|
||||
msg += ` Error message: ${JSON.stringify(error)}`
|
||||
}
|
||||
customAlert(msg)
|
||||
console.error(`LeafLLM: ${msg}`)
|
||||
|
||||
@@ -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 = [
|
||||
{ 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' }
|
||||
{ key: 'Ask', shortcut: 'Alt+A', status: 'enabled', type: 'Command' },
|
||||
{ key: 'RequestConfiguration', value: defaultConfigurations, type: 'Configuration' }
|
||||
]
|
||||
|
||||
async function sendMessage(message) {
|
||||
@@ -42,6 +74,14 @@ async function checkCommandShortcuts() {
|
||||
chrome.runtime.onInstalled.addListener((reason) => {
|
||||
if (reason.reason === chrome.runtime.OnInstalledReason.INSTALL) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user