First commit
This commit is contained in:
commit
82d2a1a495
|
@ -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