From f0a5f9317f32b74681479799976e019df854e5a9 Mon Sep 17 00:00:00 2001 From: achiyae <10044875+achiyae@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:57:38 +0300 Subject: [PATCH 1/2] phase 1 --- popup/popup.html | 6 +- popup/popup.js | 138 +++++++++++++++++++++----------------- scripts/service-worker.js | 65 +++++++++++++++--- 3 files changed, 134 insertions(+), 75 deletions(-) diff --git a/popup/popup.html b/popup/popup.html index 5e66b10..d74435e 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -26,21 +26,21 @@
Text Improvement
Text Completion
Ask
diff --git a/popup/popup.js b/popup/popup.js index 68a1296..ef60457 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,105 +1,119 @@ -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(`
${message}
`); + $('#message-box').append(`
${message}
`) } function addErrorMessage(message) { - $("#message-box").append(`
${message}
`); + $('#message-box').append(`
${message}
`) } function clearMessages() { - $("#message-box").empty(); + $('#message-box').empty() +} + + +/** + * 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 callback + */ +async function setSetting(key, value, callback = null) { + let obj = {} + obj[key] = value + 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=null] - Callback function + */ +async function getSetting(keys = null, callback = null) { + return chrome.storage.local.get(keys, callback) } async function refreshStorage() { - chrome.storage.local.get("openAIAPIKey").then(({ openAIAPIKey }) => { - $("#api-token-form .api-token-status").text(chrome.runtime.lastError || !openAIAPIKey ? "not set" : "set"); - }); + getSetting('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]); - }); - }); + getSetting(['Improve', 'Complete', 'Ask']).then(settings => + settings.forEach(({ key, shortcut, status }) => { + if (status === 'error') { + addErrorMessage(`${shortcut} could not be bound for the ${key} command. You can set it manually at chrome://extensions/shortcuts.`) + } + $(`#settings-form input[name='text-${key}']:checkbox`).prop('checked', status === 'checked') + })) } 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 }); + await chrome.storage.local.set({ openAIAPIKey }) } catch (error) { - console.log(error); - addErrorMessage("Failed to set API Token."); - return; + console.log(error) + addErrorMessage('Failed to set API Token.') + return } - await refreshStorage(); + await refreshStorage() } async function handleAPITokenClear(event) { - event.preventDefault(); - event.stopPropagation(); + event.preventDefault() + event.stopPropagation() - clearMessages(); + clearMessages() try { - await chrome.storage.local.remove("openAIAPIKey"); + await chrome.storage.local.remove('openAIAPIKey') } catch (error) { - console.log(error); - addErrorMessage("Failed to remove API Token."); - return; + console.log(error) + addErrorMessage('Failed to remove API Token.') + return } - await refreshStorage(); + 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 + getSetting(key).then(setting => { + setting.status = value ? 'checked' : 'unchecked' + setSetting(setting.key, setting) + }) + // 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(); -}); + getSetting(['Improve', 'Complete', 'Ask']).forEach(key => { + $(`#settings-form input[name='text-${name}']:checkbox`).on('change', makeHandleSettingChange(key)) + }) + return refreshStorage() +}) diff --git a/scripts/service-worker.js b/scripts/service-worker.js index 201462b..4343890 100644 --- a/scripts/service-worker.js +++ b/scripts/service-worker.js @@ -1,3 +1,9 @@ +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 +20,58 @@ 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) + } + } + }) +} + +/** + * 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 callback + */ +async function setSetting(key, value, callback = null) { + let obj = {} + obj[key] = value + 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=null] - Callback function + */ +async function getSetting(keys = null, callback = null) { + return chrome.storage.local.get(keys, callback) +} + 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 From 60b84e8229730ec1ca669c173b912b334f63e57f Mon Sep 17 00:00:00 2001 From: achiyae <10044875+achiyae@users.noreply.github.com> Date: Sat, 5 Aug 2023 15:57:08 +0300 Subject: [PATCH 2/2] phase 2 --- manifest.json | 7 ++-- popup/popup.html | 3 +- popup/popup.js | 70 +++++++++++++++------------------------ scripts/content.js | 1 + scripts/service-worker.js | 25 ++------------ scripts/utils.js | 32 ++++++++++++++++++ 6 files changed, 67 insertions(+), 71 deletions(-) create mode 100644 scripts/utils.js 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 d74435e..958f255 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -1,7 +1,8 @@ - + + diff --git a/popup/popup.js b/popup/popup.js index ef60457..3c44b99 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,3 +1,5 @@ +import {setSetting} from '../scripts/utils.js' + const apiKeyRegex = /sk-[a-zA-Z0-9]{48}/ function addMessage(message) { @@ -12,42 +14,23 @@ function clearMessages() { $('#message-box').empty() } - -/** - * 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 callback - */ -async function setSetting(key, value, callback = null) { - let obj = {} - obj[key] = value - 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=null] - Callback function - */ -async function getSetting(keys = null, callback = null) { - return chrome.storage.local.get(keys, callback) -} - async function refreshStorage() { - getSetting('openAIAPIKey').then(({ openAIAPIKey }) => { + chrome.storage.local.get('openAIAPIKey').then(({ openAIAPIKey }) => { $('#api-token-form .api-token-status').text(chrome.runtime.lastError || !openAIAPIKey ? 'not set' : 'set') }) - getSetting(['Improve', 'Complete', 'Ask']).then(settings => - settings.forEach(({ key, shortcut, status }) => { - if (status === 'error') { - addErrorMessage(`${shortcut} could not be bound for the ${key} command. You can set it manually at chrome://extensions/shortcuts.`) + 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.`) } - $(`#settings-form input[name='text-${key}']:checkbox`).prop('checked', status === 'checked') - })) + Object.values(settings).forEach(({ key, status }) => { + $(`#settings-form input[name='text-${key}']:checkbox`).prop('checked', status === 'enabled') + }) + }) } async function handleAPITokenSet(event) { @@ -66,14 +49,12 @@ async function handleAPITokenSet(event) { } 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 } - - await refreshStorage() } async function handleAPITokenClear(event) { @@ -83,14 +64,12 @@ async function handleAPITokenClear(event) { 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 } - - await refreshStorage() } function makeHandleSettingChange(key) { @@ -100,11 +79,13 @@ function makeHandleSettingChange(key) { clearMessages() const value = event.target.checked - getSetting(key).then(setting => { - setting.status = value ? 'checked' : 'unchecked' - setSetting(setting.key, setting) + 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() }) - // await refreshStorage() } } @@ -112,8 +93,9 @@ $(document).ready(async function () { $('#api-token-form .submit').on('click', handleAPITokenSet) $('#api-token-form .clear').on('click', handleAPITokenClear) - getSetting(['Improve', 'Complete', 'Ask']).forEach(key => { - $(`#settings-form input[name='text-${name}']:checkbox`).on('change', makeHandleSettingChange(key)) + let commands = ['Improve', 'Complete', 'Ask'] + commands.forEach((key) => { + $(`#settings-form input[name='text-${key}']:checkbox`).on('change', makeHandleSettingChange(key)) }) - return refreshStorage() + 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 4343890..9ee3e70 100644 --- a/scripts/service-worker.js +++ b/scripts/service-worker.js @@ -1,3 +1,5 @@ +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' }, @@ -45,29 +47,6 @@ async function checkCommandShortcuts() { }) } -/** - * 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 callback - */ -async function setSetting(key, value, callback = null) { - let obj = {} - obj[key] = value - 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=null] - Callback function - */ -async function getSetting(keys = null, callback = null) { - return chrome.storage.local.get(keys, callback) -} - async function setup() { addListener('Improve') addListener('Complete') 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