diff --git a/manifest.json b/manifest.json index 1d2e5ed..1409f28 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ }, "content_scripts": [ { - "js": ["scripts/jquery.js", "scripts/content.js"], + "js": ["scripts/jquery.js", "scripts/content.js", "scripts/utils.js"], "matches": ["https://*.overleaf.com/project/*"] } ], @@ -36,11 +36,12 @@ } }, "background": { - "service_worker": "scripts/service-worker.js" + "service_worker": "scripts/service-worker.js", + "type": "module" }, "permissions": ["storage", "tabs"], "manifest_version": 3, "name": "LeafLLM", "homepage_url": "https://github.com/achiyae/LeafLLM", - "version": "1.1.0" + "version": "1.2.0" } diff --git a/popup/popup.html b/popup/popup.html index 5e66b10..958f255 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -1,7 +1,8 @@ - + + @@ -26,21 +27,21 @@
Text Improvement
Text Completion
Ask
diff --git a/popup/popup.js b/popup/popup.js index 68a1296..3c44b99 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,105 +1,101 @@ -const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/; +import {setSetting} from '../scripts/utils.js' -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(`
${message}
`); + $('#message-box').append(`
${message}
`) } function addErrorMessage(message) { - $("#message-box").append(`
${message}
`); + $('#message-box').append(`
${message}
`) } 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]); - }); - }); + chrome.storage.local.get(['Improve', 'Complete', 'Ask']).then((settings) => { + let bindingFailures = Object.values(settings) + .filter(({ status }) => status === 'error') + .map(({ key, shortcut }) => `${shortcut} for ${key}`) + .join(', ') + if (bindingFailures.length > 0) { + addErrorMessage(`Could not bound the following shortcuts:\n${bindingFailures}.\nYou can set it manually at chrome://extensions/shortcuts.`) + } + Object.values(settings).forEach(({ key, status }) => { + $(`#settings-form input[name='text-${key}']:checkbox`).prop('checked', status === 'enabled') + }) + }) } 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 }); + chrome.storage.local.set({ openAIAPIKey }).then(refreshStorage) } catch (error) { - console.log(error); - addErrorMessage("Failed to set API Token."); - return; + console.log(error) + addErrorMessage('Failed to set API Token.') + return } - - await refreshStorage(); } async function handleAPITokenClear(event) { - event.preventDefault(); - event.stopPropagation(); + event.preventDefault() + event.stopPropagation() - clearMessages(); + clearMessages() try { - await chrome.storage.local.remove("openAIAPIKey"); + chrome.storage.local.remove('openAIAPIKey').then(refreshStorage) } catch (error) { - console.log(error); - addErrorMessage("Failed to remove API Token."); - return; + console.log(error) + addErrorMessage('Failed to remove API Token.') + return } - - await refreshStorage(); } 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; - } - - await refreshStorage(); - }; + const value = event.target.checked + chrome.storage.local.get(key).then(setting => { + if(setting[key].status !== 'error') { + setting[key].status = value ? 'enabled' : 'disabled' + setSetting(setting[key].key, setting[key]) + } + return 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)) + }) + await refreshStorage() +}) diff --git a/scripts/content.js b/scripts/content.js index 5911a11..beea07a 100644 --- a/scripts/content.js +++ b/scripts/content.js @@ -1,3 +1,4 @@ + class OpenAIAPI { static defaultModel = 'text-davinci-003' diff --git a/scripts/service-worker.js b/scripts/service-worker.js index 201462b..9ee3e70 100644 --- a/scripts/service-worker.js +++ b/scripts/service-worker.js @@ -1,3 +1,11 @@ +import {setSetting} from './utils.js' + +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 @@ -14,19 +22,35 @@ function addListener(commandName) { }) } +chrome.runtime.onInstalled.addListener((reason) => { + if (reason.reason === chrome.runtime.OnInstalledReason.INSTALL) { + checkCommandShortcuts() + } +}) + +// Only use this function during the initial install 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' + } + setSetting(command.key, command) + } + } + }) +} + 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() +setup() \ No newline at end of file diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..e3b55ed --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,32 @@ +/** + * Set a setting in storage {@link https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageArea:~:text=to%20the%20callback.-,set,-void} + * @param key + * @param value + * @param {function}[callback] Optional callback function + */ +export async function setSetting(key, value, callback) { + let obj = {} + obj[key] = value + if (typeof callback === 'undefined') { + chrome.storage.local.set(obj).catch(error => { + console.log(`Failed to set ${key} setting. Error: ${error}`) + }) + } else { + chrome.storage.local.set(obj, callback).catch(error => { + console.log(`Failed to set ${key} setting. Error: ${error}`) + }) + } +} + +/** + * Get a setting from storage + * @param {string | string[] | object} [keys=null] - The keys to get (see {@link https://developer.chrome.com/docs/extensions/reference/storage/#usage}) + * @param {function}[callback] Optional callback function + */ +export async function getSetting(keys = null, callback) { + if (typeof callback === 'undefined') { + return chrome.storage.local.get(keys, (items) => Object.values(items)) + }else { + return chrome.storage.local.get(keys, (items) => callback(Object.values(items))) + } +} \ No newline at end of file