First commit

This commit is contained in:
Eric Pelland 2023-01-07 22:00:40 -05:00
commit 82d2a1a495
14 changed files with 18160 additions and 0 deletions

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)