First commit
This commit is contained in:
commit
16d68c1a4f
|
@ -0,0 +1,57 @@
|
|||
# AI Browser Assistant
|
||||
![Tooltip](./icon.png)
|
||||
|
||||
The AI Browser Assistant is a Google Chrome browser extension that uses advanced AI technology from OpenAI to help users understand and interact with text on the web. With just a few clicks, users can:
|
||||
|
||||
- Translate selected text into a different language
|
||||
- Summarize selected text to get the main points
|
||||
- Complete selected text by suggesting the next word or phrase
|
||||
- Respond to selected text with a relevant comment or answer
|
||||
- Explain selected text by providing additional context and information
|
||||
- Chat directly with the AI model using the built-in chat feature
|
||||
|
||||
## Getting Started
|
||||
|
||||
To use the AI Browser Assistant, you will need to obtain an API key from OpenAI. You can sign up for a free API key at the [OpenAI Developer Portal](https://beta.openai.com/signup/). Once you have your API key, follow these steps to get started:
|
||||
|
||||
1. Install the AI Browser Assistant from the [Chrome Web Store](https://chrome.google.com/webstore/). (COMING SOON - For now turn on developer mode in your extension settings, and load it unpacked)
|
||||
2. Click on the extension icon to open the popup window.
|
||||
3. Go to the Settings tab.
|
||||
4. Enter your API key in the API Key field.
|
||||
5. Click the Update Settings button to save your API key.
|
||||
|
||||
![settings](./README_images/settings.png)
|
||||
|
||||
## Customizing the AI Model and Settings
|
||||
|
||||
In the Settings tab, you can customize the following options:
|
||||
|
||||
- **Model:** Select the AI model that you want to use for translations, summaries, completions, responses, and explanations.
|
||||
- **Temperature:** Adjust the temperature of the AI model to control the creativity and randomness of its output.
|
||||
- **Max Tokens:** Set the maximum number of tokens (words and punctuation) that the AI model should generate.
|
||||
- **Top P:** Set the top P value to control the proportion of the mass of the distribution that the AI model should generate.
|
||||
- **Frequency Penalty:** Adjust the frequency penalty to control the likelihood of the AI model generating frequent words and phrases.
|
||||
- **Presence Penalty:** Adjust the presence penalty to control the likelihood of the AI model generating words and phrases that are not present in the input text.
|
||||
|
||||
## Using the AI Browser Assistant
|
||||
|
||||
To use the AI Browser Assistant, simply highlight the text that you want to translate, summarize, complete, respond to, or explain, then right-click and select the appropriate option from the context menu.
|
||||
|
||||
![Context](./README_images/context.png)
|
||||
![Tooltip](./README_images/tooltip.png)
|
||||
|
||||
You can also click on the extension icon to open the popup window and use the chat feature to communicate directly with the AI model.
|
||||
|
||||
![Chat](./README_images/chat.png)
|
||||
|
||||
## Notes
|
||||
- Created in one sitting session utilizing chatGPT. Believe it or not most of this project was AI generated including this entire readme (except this line). I personally made some modifications here and there, and spent time doing a refactor at the end.
|
||||
- Some features of the AI Browser Assistant may not be available for certain languages or models.
|
||||
- The AI model is trained on a large dataset of text, and may generate responses that are inappropriate or offensive. Use at your own risk.
|
||||
|
||||
## Credits
|
||||
|
||||
- AI technology from [OpenAI](https://openai.com/)
|
||||
- [jQuery](https://jquery.com/)
|
||||
- [Bootstrap](https://getbootstrap.com/)
|
||||
- [Font Awesome](https://fontawesome.com/)
|
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
|
@ -0,0 +1,157 @@
|
|||
let selectedMenuItemId = null
|
||||
let selectedText = null
|
||||
let settings = {
|
||||
model: "text-davinci-003",
|
||||
temperature: 0.7,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
api_key: "apikeyhere"
|
||||
}
|
||||
|
||||
const createContextMenu = (title) => {
|
||||
chrome.contextMenus.create({
|
||||
id: title.toLowerCase().split(" ").join("-"),
|
||||
title: title,
|
||||
contexts: ["browser_action", "selection"]
|
||||
})
|
||||
}
|
||||
|
||||
const generatePrompt = () => {
|
||||
let prompt
|
||||
switch (selectedMenuItemId) {
|
||||
case "explain-selection":
|
||||
prompt = "Explain: '" + selectedText + "'"
|
||||
break
|
||||
case "complete-selection":
|
||||
prompt = selectedText
|
||||
break
|
||||
case "respond-to-selection":
|
||||
prompt = "Respond to this message: '" + selectedText + "'"
|
||||
break
|
||||
case "summerize-selection":
|
||||
prompt = "Summerize: '" + selectedText + "'"
|
||||
break
|
||||
case "translate-selection":
|
||||
// TODO: Add language selection to the settings on the popup.js file.
|
||||
prompt = "Translate into english: '" + selectedText + "'"
|
||||
break
|
||||
default:
|
||||
prompt = selectedText
|
||||
break
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
|
||||
const sendTabMessage = (message) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
if (tabs[0]) {
|
||||
chrome.tabs.sendMessage(tabs[0].id, message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const generateRequestBody = (prompt) => {
|
||||
return JSON.stringify({ ...Object.fromEntries(Object.entries(settings).filter(([key]) => key != "api_key")), ...{ prompt: prompt } })
|
||||
}
|
||||
|
||||
const handleAPIResponse = (json) => {
|
||||
let apiResult = json.choices[0].text
|
||||
|
||||
if (selectedMenuItemId == null) {
|
||||
// send to popup.js
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'prompt-response',
|
||||
data: apiResult
|
||||
})
|
||||
} else {
|
||||
// send to content.js
|
||||
sendTabMessage({ apiResult: apiResult })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAPIError = (error) => {
|
||||
sendTabMessage({ apiResult: "Error. Please retry." })
|
||||
}
|
||||
|
||||
const executeAPICall = (prompt) => {
|
||||
fetch("https://api.openai.com/v1/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + settings.api_key
|
||||
},
|
||||
body: generateRequestBody(prompt)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(handleAPIResponse).catch(handleAPIError)
|
||||
}
|
||||
|
||||
const setLoading = () => {
|
||||
if (selectedMenuItemId) {
|
||||
sendTabMessage({ apiResult: "loading" })
|
||||
}
|
||||
}
|
||||
|
||||
const requestAPIResponse = () => {
|
||||
if (!selectedText) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading()
|
||||
executeAPICall(generatePrompt())
|
||||
}
|
||||
|
||||
const handleContextMenuClick = (info, tab) => {
|
||||
selectedMenuItemId = info.menuItemId
|
||||
requestAPIResponse()
|
||||
}
|
||||
|
||||
const handleSettingsLoad = (storageResult) => {
|
||||
if (storageResult.settings) {
|
||||
setSettings(storageResult.settings)
|
||||
}
|
||||
}
|
||||
|
||||
const setSettings = (newSettings) => {
|
||||
settings = {
|
||||
model: newSettings.model,
|
||||
temperature: parseFloat(newSettings.temperature),
|
||||
max_tokens: parseInt(newSettings.max_tokens),
|
||||
top_p: parseFloat(newSettings.top_p),
|
||||
frequency_penalty: parseFloat(newSettings.frequency_penalty),
|
||||
presence_penalty: parseFloat(newSettings.presence_penalty),
|
||||
api_key: newSettings.api_key
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessage = (message, sender, sendResponse) => {
|
||||
// TODO: Use a message type
|
||||
if (message.hasOwnProperty("selectedText")) {
|
||||
selectedText = message.selectedText
|
||||
if (!selectedText || selectedText === "") {
|
||||
sendTabMessage({ removeTooltip: true })
|
||||
}
|
||||
} else if (message.hasOwnProperty("model")) {
|
||||
setSettings(message)
|
||||
} else if (message.hasOwnProperty("retry")) {
|
||||
requestAPIResponse()
|
||||
} else if (message.hasOwnProperty("prompt")) {
|
||||
selectedText = message.prompt
|
||||
selectedMenuItemId = null
|
||||
requestAPIResponse()
|
||||
}
|
||||
}
|
||||
|
||||
createContextMenu("Explain Selection")
|
||||
createContextMenu("Complete Selection")
|
||||
createContextMenu("Respond to Selection")
|
||||
createContextMenu("Summerize Selection")
|
||||
createContextMenu("Translate Selection")
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(handleContextMenuClick)
|
||||
|
||||
chrome.storage.sync.get("settings", handleSettingsLoad)
|
||||
|
||||
chrome.runtime.onMessage.addListener(handleMessage)
|
|
@ -0,0 +1,116 @@
|
|||
// Keep track of the current tooltip element
|
||||
let currentTooltip = null
|
||||
|
||||
const removeTooltip = () => {
|
||||
// Remove the current tooltip element, if there is one
|
||||
if (currentTooltip) {
|
||||
document.body.removeChild(currentTooltip)
|
||||
currentTooltip = null
|
||||
}
|
||||
}
|
||||
|
||||
const addLoadingIcon = (tooltip) => {
|
||||
// Create a loading element
|
||||
let loadingElement = document.createElement("div")
|
||||
loadingElement.classList.add("loading-ai-tooltip")
|
||||
|
||||
// Add a spinner animation to the loading element
|
||||
let spinner = document.createElement("div")
|
||||
spinner.classList.add("spinner-ai-tooltip")
|
||||
loadingElement.appendChild(spinner)
|
||||
|
||||
// Add the loading element to the tooltip
|
||||
tooltip.appendChild(loadingElement)
|
||||
}
|
||||
|
||||
const handleRetryButtonClick = () => {
|
||||
chrome.runtime.sendMessage({ retry: true })
|
||||
}
|
||||
|
||||
|
||||
const createButton = (text, callback) => {
|
||||
let button = document.createElement("button")
|
||||
button.innerText = text
|
||||
button.onclick = callback
|
||||
return button
|
||||
}
|
||||
|
||||
const addButtons = (tooltip, apiResult) => {
|
||||
let retryButton = createButton("Retry", handleRetryButtonClick)
|
||||
let copyButton = createButton("Copy", () => { navigator.clipboard.writeText(apiResult) })
|
||||
|
||||
tooltip.appendChild(copyButton)
|
||||
tooltip.appendChild(retryButton)
|
||||
}
|
||||
|
||||
const addTooltip = (apiResult) => {
|
||||
let tooltip = document.createElement("div")
|
||||
tooltip.classList.add("ai-tooltip")
|
||||
|
||||
if (apiResult == 'loading') {
|
||||
addLoadingIcon(tooltip)
|
||||
} else {
|
||||
tooltip.innerText = apiResult
|
||||
addButtons(tooltip, apiResult)
|
||||
}
|
||||
|
||||
document.body.appendChild(tooltip)
|
||||
currentTooltip = tooltip
|
||||
adjustTooltipPosition()
|
||||
}
|
||||
|
||||
const adjustTooltipPosition = () => {
|
||||
if (!currentTooltip) {
|
||||
return
|
||||
}
|
||||
|
||||
let rect = window.getSelection().getRangeAt(0).getBoundingClientRect()
|
||||
currentTooltip.style.cssText = "top: " + rect.bottom + "px !important; left: " + rect.left + "px !important;"
|
||||
}
|
||||
|
||||
const handleAPIResult = (apiResult) => {
|
||||
removeTooltip()
|
||||
addTooltip(apiResult)
|
||||
}
|
||||
|
||||
const messageHandler = (message) => {
|
||||
if (message.hasOwnProperty("removeTooltip")) {
|
||||
removeTooltip()
|
||||
} else {
|
||||
console.log(message)
|
||||
handleAPIResult(message.apiResult.trim())
|
||||
}
|
||||
}
|
||||
|
||||
const sendRuntimeMessage = () => {
|
||||
let selectedText = window.getSelection().toString()
|
||||
chrome.runtime.sendMessage({ selectedText: selectedText })
|
||||
}
|
||||
|
||||
const debounce = (func, wait) => {
|
||||
let timeout
|
||||
return (...args) => {
|
||||
const later = () => {
|
||||
timeout = null
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
setTimeout(adjustTooltipPosition, 300)
|
||||
}
|
||||
|
||||
// Listen for messages from the background script
|
||||
chrome.runtime.onMessage.addListener(messageHandler)
|
||||
|
||||
// Create a debounced version of the event listener
|
||||
let debouncedSendRuntimeMessage = debounce(sendRuntimeMessage, 250)
|
||||
|
||||
// Add the debounced event listener
|
||||
document.addEventListener("selectionchange", debouncedSendRuntimeMessage)
|
||||
|
||||
// Fix the position of the tooltip when the page is scrolled
|
||||
window.addEventListener("wheel", handleScroll)
|
Binary file not shown.
After Width: | Height: | Size: 823 B |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,37 @@
|
|||
.loading-ai-tooltip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner-ai-tooltip {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #ccc;
|
||||
border-radius: 50%;
|
||||
border-top-color: #333;
|
||||
animation: spinner-ai-tooltip 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner-ai-tooltip {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-tooltip {
|
||||
all: initial;
|
||||
color: black !important;
|
||||
z-index: 99999;
|
||||
font-size: 14px !important;
|
||||
max-width: 500px !important;
|
||||
min-height: 30px;
|
||||
min-width: 50px;
|
||||
max-height: 200px !important;
|
||||
overflow-y: auto !important;
|
||||
position: fixed !important;
|
||||
background-color: lightyellow !important;
|
||||
border: 1px solid gray !important;
|
||||
padding: 5px !important;
|
||||
border-radius: 15px !important;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AI Browser Assistant",
|
||||
"description": "OpenAI powered Assistant",
|
||||
"version": "0.0.1",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://api.openai.com/*",
|
||||
"<all_urls>"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"fix.css"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"css": [
|
||||
"fix.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_icon": "icon.png",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "favicon-16x16.png",
|
||||
"32": "favicon-32x32.png"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>My Chrome Extension</title>
|
||||
<!-- Add the FontAwesome and Bootstrap links -->
|
||||
<link rel="stylesheet" href="./includes/all.min.css">
|
||||
<link rel="stylesheet" href="./includes/bootstrap.min.css">
|
||||
</head>
|
||||
|
||||
<body style="width: 500px;">
|
||||
<!-- Create the tabbed interface -->
|
||||
<div class="tabs nav nav-tabs" role="tablist">
|
||||
<a class="nav-item nav-link active" id="tab1" data-toggle="tab" href="#tab1-content" role="tab">
|
||||
<i class="fas fa-search"></i>
|
||||
</a>
|
||||
<a class="nav-item nav-link" id="tab2" data-toggle="tab" href="#tab2-content" role="tab">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Add the tab content -->
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="tab1-content" role="tabpanel">
|
||||
<!-- Add a text area and a submit button -->
|
||||
<textarea id="text-area" readonly class="form-control"
|
||||
style="max-height: 500px; height: 200px; white-space: pre-wrap;"></textarea>
|
||||
<input type="text" id="text-input" class="form-control" placeholder="Start chatting here">
|
||||
</div>
|
||||
<div class="tab-pane" id="tab2-content" role="tabpanel">
|
||||
<!-- Settings -->
|
||||
<button id="reset" class="btn btn-secondary" style="float: right">Reset</button>
|
||||
<form id="settings-form" class="form-group"
|
||||
style="padding: 22px; width: 400px; margin-left: auto; margin-right: auto;">
|
||||
<label for="api_key" class="control-label">API KEY:</label>
|
||||
<input type="password" id="api_key" name="api_key" class="form-control">
|
||||
<br>
|
||||
<label for="model" class="control-label">Model:</label>
|
||||
<select id="model" name="model" class="form-control">
|
||||
<option value="text-davinci-002">text-davinci-002</option>
|
||||
<option value="text-davinci-003" selected>text-davinci-003</option>
|
||||
<option value="text-curie-001">text-curie-001</option>
|
||||
<option value="text-babbage-001">text-babbage-001</option>
|
||||
<option value="text-ada-001">text-ada-001</option>
|
||||
</select>
|
||||
<br>
|
||||
<label for="temperature" class="control-label">Temperature:</label>
|
||||
<input type="number" id="temperature" name="temperature" min="0" max="1" step="0.1" value="0.7"
|
||||
class="form-control">
|
||||
<br>
|
||||
<label for="max_tokens" class="control-label">Max Tokens:</label>
|
||||
<input type="number" id="max_tokens" name="max_tokens" min="1" max="2048" value="256" class="form-control">
|
||||
<br>
|
||||
<label for="top_p" class="control-label">Top P:</label>
|
||||
<input type="number" id="top_p" name="top_p" min="0" max="1" step="0.1" value="1" class="form-control">
|
||||
<br>
|
||||
<label for="frequency_penalty" class="control-label">Frequency Penalty:</label>
|
||||
<input type="number" id="frequency_penalty" name="frequency_penalty" min="0" max="1" step="0.1" value="0"
|
||||
class="form-control">
|
||||
<br>
|
||||
<label for="presence_penalty" class="control-label">Presence Penalty:</label>
|
||||
<input type="number" id="presence_penalty" name="presence_penalty" min="0" max="1" step="0.1" value="0"
|
||||
class="form-control">
|
||||
<br>
|
||||
<button type="submit" class="btn btn-primary">Update Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add the Bootstrap and FontAwesome scripts -->
|
||||
<script src="./includes/jquery.slim.min.js"></script>
|
||||
<script src="./includes/bootstrap.bundle.min.js"></script>
|
||||
<script src="./includes/all.min.js"></script>
|
||||
<!-- Add the custom script file -->
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,98 @@
|
|||
// Set the default settings for the OpenAI API call
|
||||
let defaultSettings = {
|
||||
model: "text-davinci-003",
|
||||
temperature: 0.7,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
api_key: ''
|
||||
}
|
||||
|
||||
let settings = { ...defaultSettings }
|
||||
|
||||
const gebid = (id) => {
|
||||
return document.getElementById(id)
|
||||
}
|
||||
|
||||
const setSettingsInputValues = () => {
|
||||
gebid("model").value = settings.model
|
||||
gebid("temperature").value = settings.temperature
|
||||
gebid("max_tokens").value = settings.max_tokens
|
||||
gebid("top_p").value = settings.top_p
|
||||
gebid("frequency_penalty").value = settings.frequency_penalty
|
||||
gebid("presence_penalty").value = settings.presence_penalty
|
||||
gebid("api_key").value = settings.api_key
|
||||
}
|
||||
|
||||
const handleSettingsLoaded = (storageResult) => {
|
||||
if (storageResult.settings) {
|
||||
settings = storageResult.settings
|
||||
}
|
||||
// Set the default settings in the UI
|
||||
setSettingsInputValues()
|
||||
}
|
||||
|
||||
const resetSettings = () => {
|
||||
settings = { ...defaultSettings, ...{ api_key: settings.api_key } }
|
||||
setSettingsInputValues()
|
||||
chrome.runtime.sendMessage(settings)
|
||||
chrome.storage.sync.set({ settings: settings })
|
||||
}
|
||||
|
||||
const updateSettings = (form) => {
|
||||
// Get the values of the form fields
|
||||
settings.model = form.elements.model.value
|
||||
settings.temperature = form.elements.temperature.value
|
||||
settings.max_tokens = form.elements.max_tokens.value
|
||||
settings.top_p = form.elements.top_p.value
|
||||
settings.frequency_penalty = form.elements.frequency_penalty.value
|
||||
settings.presence_penalty = form.elements.presence_penalty.value
|
||||
settings.api_key = form.elements.api_key.value
|
||||
|
||||
// Send a message to the background script with the form field values
|
||||
chrome.runtime.sendMessage(settings)
|
||||
chrome.storage.sync.set({ settings: settings })
|
||||
}
|
||||
|
||||
const handleSettingsSaveEvent = (event) => {
|
||||
event.preventDefault()
|
||||
updateSettings(gebid("settings-form"))
|
||||
}
|
||||
|
||||
const handleChatSubmiittion = () => {
|
||||
// Get the text from the textarea
|
||||
let textArea = gebid('text-area')
|
||||
textArea.value += "\n" + gebid('text-input').value + "\n"
|
||||
gebid('text-input').value = ""
|
||||
textArea.scrollTop = textArea.scrollHeight
|
||||
|
||||
// Send the text to the background script
|
||||
chrome.runtime.sendMessage({ prompt: textArea.value })
|
||||
}
|
||||
|
||||
const messageHandler = (message, sender, sendResponse) => {
|
||||
if (message.type === 'prompt-response') {
|
||||
// Update the text in the textarea
|
||||
let textArea = gebid('text-area')
|
||||
textArea.value += message.data + "\n"
|
||||
textArea.scrollTop = textArea.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeydownEvent = (event) => {
|
||||
if (event.code === 'Enter') {
|
||||
handleChatSubmiittion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
chrome.storage.sync.get("settings", handleSettingsLoaded)
|
||||
|
||||
gebid("settings-form").addEventListener("submit", handleSettingsSaveEvent)
|
||||
|
||||
gebid("reset").addEventListener("click", resetSettings)
|
||||
|
||||
chrome.runtime.onMessage.addListener(messageHandler)
|
||||
|
||||
gebid('text-input').addEventListener('keydown', handleKeydownEvent)
|
Loading…
Reference in New Issue