First commit

This commit is contained in:
Eric Pelland 2023-01-07 22:00:40 -05:00
commit 25b7c57a32
20 changed files with 18217 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

57
README.md Normal file
View File

@ -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/).
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/)

BIN
README_images/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
README_images/context.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
README_images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
README_images/tooltip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

157
background.js Normal file
View File

@ -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)

116
content.js Normal file
View File

@ -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)

BIN
favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

BIN
favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

37
fix.css Normal file
View File

@ -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;
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

6143
includes/all.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

5
includes/all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
includes/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

11470
includes/bootstrap.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

2
includes/jquery.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

49
manifest.json Normal file
View File

@ -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"
}
}

76
popup.html Normal file
View File

@ -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>

98
popup.js Normal file
View File

@ -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)