Initial commit
This commit is contained in:
94
.gitignore
vendored
Normal file
94
.gitignore
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all
|
||||||
|
|
||||||
|
### Intellij+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Intellij+all Patch ###
|
||||||
|
# Ignore everything but code style settings and run configurations
|
||||||
|
# that are supposed to be shared within teams.
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
!.idea/codeStyles
|
||||||
|
!.idea/runConfigurations
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/intellij+all
|
||||||
|
|
||||||
|
LeafLLM*.zip
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# LeafLLM: an AI-powered Overleaf
|
||||||
|
This Chrome extension adds the power of large-language models (LLMs) to Overleaf through a Chrome extension.
|
||||||
|
|
||||||
|
The extension originated from [GPT4Overleaf](https://github.com/e3ntity/gpt4overleaf).
|
||||||
|
|
||||||
|
## Manual installation
|
||||||
|
1. Clone the repository
|
||||||
|
2. Open Chrome and go to `chrome://extensions/`
|
||||||
|
3. Enable developer mode
|
||||||
|
4. Click "Load unpacked" and select the repository folder
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
The plugin can be configured by clicking the plugin button in the Chrome toolbar. It requires inserting an API key from [OpenAI](https://platform.openai.com/account/api-keys). You also need to choose which tools you wish to enable.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
These are the tools that are currently available:
|
||||||
|
|
||||||
|
### Auto-complete
|
||||||
|
Select a text and press `Alt+c` to trigger the auto-complete tool.
|
||||||
|
|
||||||
|
### Improve
|
||||||
|
Select a text and press `Alt+i` to trigger the improvement tool. The original text will be commented out and the improved text will be inserted below it.
|
||||||
|
|
||||||
|
### Ask
|
||||||
|
Select a text and press `Alt+a` to trigger the ask tool. The original text will be deleted and the answer will be inserted in its place.
|
||||||
|
|
||||||
|
For example: "Create a table 4x3 that the first row is bold face" will be replaced with, e.g.,:
|
||||||
|
```latex
|
||||||
|
\begin{tabular}{|c|c|c|}
|
||||||
|
\hline
|
||||||
|
\textbf{Column 1} & \textbf{Column 2} & \textbf{Column 3}\\
|
||||||
|
\hline
|
||||||
|
Entry 1 & Entry 2 & Entry 3\\
|
||||||
|
\hline
|
||||||
|
Entry 4 & Entry 5 & Entry 6\\
|
||||||
|
\hline
|
||||||
|
Entry 7 & Entry 8 & Entry 9\\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then, for example:
|
||||||
|
1. Write before the table: Place the following tabular inside a table environment, center it, and give the following title: "The comparison of the three approaches"
|
||||||
|
2. Select the sentence and the table
|
||||||
|
3. Press `Alt+a` to trigger the ask tool.
|
||||||
|
|
||||||
|
The result will be:
|
||||||
|
```latex
|
||||||
|
\begin{table}[h]
|
||||||
|
\centering
|
||||||
|
\caption{The comparison of the three approaches}
|
||||||
|
\begin{tabular}{|c|c|c|}
|
||||||
|
\hline
|
||||||
|
\textbf{Column 1} & \textbf{Column 2} & \textbf{Column 3}\\
|
||||||
|
\hline
|
||||||
|
Entry 1 & Entry 2 & Entry 3\\
|
||||||
|
\hline
|
||||||
|
Entry 4 & Entry 5 & Entry 6\\
|
||||||
|
\hline
|
||||||
|
Entry 7 & Entry 8 & Entry 9\\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
If you encounter any issues, please open an issue in the project's repository.
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
The plugin does not collect any data. The only data that is sent is the text that you select and the API key that you provide. The data is sent to OpenAI's servers only.
|
||||||
22
deploy.ps1
Normal file
22
deploy.ps1
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Define the output ZIP file name
|
||||||
|
$zipFileName = "LeafLLM.zip"
|
||||||
|
|
||||||
|
# Get the current directory path
|
||||||
|
$currentDirectory = Get-Location
|
||||||
|
|
||||||
|
# Delete the ZIP file if it already exists
|
||||||
|
if (Test-Path $zipFileName) {
|
||||||
|
Remove-Item $zipFileName -Force
|
||||||
|
Write-Host "Existing ZIP file '$zipFileName' deleted."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create an array of directories and files to exclude from the ZIP
|
||||||
|
$excludeItems = @(".idea", ".git", ".gitignore", "deploy.sh", "deploy.ps1")
|
||||||
|
|
||||||
|
# Get the files and folders in the current directory excluding the specified items
|
||||||
|
$itemsToZip = Get-ChildItem -Path $currentDirectory -Exclude $excludeItems
|
||||||
|
|
||||||
|
# Compress the items to a ZIP archive
|
||||||
|
Compress-Archive -Path $itemsToZip.FullName -DestinationPath $zipFileName
|
||||||
|
|
||||||
|
Write-Host "ZIP file created: $zipFileName"
|
||||||
3
deploy.sh
Normal file
3
deploy.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
echo "Packaging extension for submission to the Chrome Web Store."
|
||||||
|
|
||||||
|
zip -r LeafLLM.zip ./* -x .git/**\* -x .idea/**\* -x .gitignore -x deploy.sh -x README.md
|
||||||
46
manifest.json
Normal file
46
manifest.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup/popup.html",
|
||||||
|
"default_icon": "popup/icon.png"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": ["scripts/jquery.js", "scripts/content.js"],
|
||||||
|
"matches": ["https://*.overleaf.com/project/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "LLM-based tools for Overleaf",
|
||||||
|
"icons": {
|
||||||
|
"16": "popup/icon_16.png",
|
||||||
|
"48": "popup/icon_48.png",
|
||||||
|
"128": "popup/icon_128.png"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"Complete": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "Complete selected text"
|
||||||
|
},
|
||||||
|
"Improve": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+I"
|
||||||
|
},
|
||||||
|
"description": "Improve selected text"
|
||||||
|
},
|
||||||
|
"Ask": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+A"
|
||||||
|
},
|
||||||
|
"description": "Use the selected text to ask GPT. It adds to the beginning of the selected text: 'In Latex, '"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "scripts/service-worker.js"
|
||||||
|
},
|
||||||
|
"permissions": ["storage", "tabs"],
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "LeafLLM",
|
||||||
|
"homepage_url": "https://github.com/achiyae/LeafLLM",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
BIN
popup/gpt4overleaf_screenshot.png
Normal file
BIN
popup/gpt4overleaf_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
popup/icon.png
Normal file
BIN
popup/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
popup/icon_128.png
Normal file
BIN
popup/icon_128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
popup/icon_16.png
Normal file
BIN
popup/icon_16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
popup/icon_48.png
Normal file
BIN
popup/icon_48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
175
popup/popup.css
Normal file
175
popup/popup.css
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extension-title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-token-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-token-status {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-token-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px 0;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box .error {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box .message {
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input span:first-child {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
.settings-form {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-text {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
58
popup/popup.html
Normal file
58
popup/popup.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="/scripts/jquery.js"></script>
|
||||||
|
<script src="/popup/popup.js"></script>
|
||||||
|
<link rel="stylesheet" href="/popup/popup.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 class="extension-title">LeafLLM: an AI-powered Overleaf</h1>
|
||||||
|
<div id="controls" class="form-container">
|
||||||
|
<div class="control">
|
||||||
|
<div class="input"><span>ALT</span><span>I</span></div>
|
||||||
|
<div class="effect">Improve selected text.</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="input"><span>ALT</span><span>C</span></div>
|
||||||
|
<div class="effect">Complete selected text.</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="input"><span>ALT</span><span>A</span></div>
|
||||||
|
<div class="effect">Ask GPT.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="settings-form" class="form-container">
|
||||||
|
<div class="settings-text">Settings</div>
|
||||||
|
<div class="settings-container">
|
||||||
|
<div class="settings-item">
|
||||||
|
<div class="settings-item-text">Text Improvement</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input name="text-improvement" class="settings-item-checkbox" type="checkbox" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<div class="settings-item-text">Text Completion</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input name="text-completion" class="settings-item-checkbox" type="checkbox" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<div class="settings-item-text">Ask</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input name="text-ask" class="settings-item-checkbox" type="checkbox" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="api-token-form" class="form-container">
|
||||||
|
<div class="api-token-text"><span>API Token is </span><span class="api-token-status">not set</span></div>
|
||||||
|
<input class="api-token-input" name="api-token" placeholder="OpenAI API Token" type="text" />
|
||||||
|
<div class="message">To get an API key, go to <a href="https://platform.openai.com/account/api-keys">https://platform.openai.com/account/api-keys</a> </div>
|
||||||
|
<button class="submit btn">Set API Token</button>
|
||||||
|
<button class="clear btn">Reset API Token</button>
|
||||||
|
</div>
|
||||||
|
<div id="message-box" class="message-box"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
105
popup/popup.js
Normal file
105
popup/popup.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function addMessage(message) {
|
||||||
|
$("#message-box").append(`<div class="message">${message}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addErrorMessage(message) {
|
||||||
|
$("#message-box").append(`<div class="error">${message}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMessages() {
|
||||||
|
$("#message-box").empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStorage() {
|
||||||
|
chrome.storage.local.get("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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAPITokenSet(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
clearMessages();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.set({ openAIAPIKey });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
addErrorMessage("Failed to set API Token.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAPITokenClear(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
clearMessages();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.storage.local.remove("openAIAPIKey");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
addErrorMessage("Failed to remove API Token.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHandleSettingChange(key) {
|
||||||
|
return async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(async function () {
|
||||||
|
$("#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();
|
||||||
|
});
|
||||||
169
scripts/content.js
Normal file
169
scripts/content.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
class OpenAIAPI {
|
||||||
|
static defaultModel = 'text-davinci-003'
|
||||||
|
|
||||||
|
constructor(apiKey) {
|
||||||
|
this.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
query(endpoint, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = `https://api.openai.com/v1/${endpoint}`
|
||||||
|
|
||||||
|
if (!data.model) data.model = OpenAIAPI.defaultModel
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('POST', url, true)
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||||
|
xhr.setRequestHeader('Authorization', `Bearer ${this.apiKey}`)
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState !== 4) return
|
||||||
|
if (xhr.status !== 200) return reject('Failed to query OpenAI API.')
|
||||||
|
|
||||||
|
const jsonResponse = JSON.parse(xhr.responseText)
|
||||||
|
|
||||||
|
if (!jsonResponse.choices) return reject('Failed to query OpenAI API.')
|
||||||
|
|
||||||
|
return resolve(jsonResponse.choices)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(JSON.stringify(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeText(text) {
|
||||||
|
const data = {
|
||||||
|
max_tokens: 512,
|
||||||
|
prompt: text,
|
||||||
|
n: 1,
|
||||||
|
temperature: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.query('completions', data)
|
||||||
|
|
||||||
|
return result[0].text
|
||||||
|
}
|
||||||
|
|
||||||
|
async improveText(text) {
|
||||||
|
const data = {
|
||||||
|
model: 'code-davinci-edit-001',
|
||||||
|
input: text,
|
||||||
|
instruction:
|
||||||
|
'Correct any spelling mistakes, grammar mistakes, and improve the overall style of the (latex) text.',
|
||||||
|
n: 1,
|
||||||
|
temperature: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.query('edits', data)
|
||||||
|
|
||||||
|
return result[0].text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSelectedText(replacementText, selection) {
|
||||||
|
const sel = selection === undefined ? window.getSelection() : selection
|
||||||
|
|
||||||
|
if (sel.rangeCount) {
|
||||||
|
const range = sel.getRangeAt(0)
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(document.createTextNode(replacementText))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settingIsEnabled(setting) {
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
result = await chrome.storage.local.get(setting)
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return result[setting]
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentText(text) {
|
||||||
|
const regexPattern = /\n/g
|
||||||
|
const replacementString = '\n%'
|
||||||
|
let comment = text.replace(regexPattern, replacementString)
|
||||||
|
if (!comment.startsWith('%')) {
|
||||||
|
comment = '%' + comment
|
||||||
|
}
|
||||||
|
return comment
|
||||||
|
}
|
||||||
|
|
||||||
|
async function improveTextHandler(openAI) {
|
||||||
|
if (!(await settingIsEnabled('textImprovement'))) return
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const selectedText = selection.toString()
|
||||||
|
if (!selectedText) return
|
||||||
|
const editedText = await openAI.improveText(selectedText)
|
||||||
|
const commentedText = commentText(selectedText)
|
||||||
|
replaceSelectedText(commentedText + '\n' + editedText, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeTextHandler(openAI) {
|
||||||
|
if (!(await settingIsEnabled('textCompletion'))) return
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const selectedText = selection.toString()
|
||||||
|
if (!selectedText) return
|
||||||
|
const editedText = await openAI.completeText(selectedText)
|
||||||
|
replaceSelectedText(selectedText + editedText, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askHandler(openAI) {
|
||||||
|
if (!(await settingIsEnabled('textAsk'))) return
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const selectedText = selection.toString()
|
||||||
|
if (!selectedText) return
|
||||||
|
const editedText = await openAI.completeText('In Latex, ' + selectedText)
|
||||||
|
replaceSelectedText(editedText, selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentAPIKey
|
||||||
|
let openAI = undefined
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAPIKey(key) {
|
||||||
|
if (currentAPIKey === key) return
|
||||||
|
cleanup()
|
||||||
|
currentAPIKey = key
|
||||||
|
if (currentAPIKey) {
|
||||||
|
openAI = new OpenAIAPI(currentAPIKey)
|
||||||
|
console.log('AI4Overleaf: OpenAI API key set, enabling AI4Overleaf features.')
|
||||||
|
} else {
|
||||||
|
openAI = undefined
|
||||||
|
console.log('AI4Overleaf: OpenAI API key is not set, AI4Overleaf features are disabled.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommand(command) {
|
||||||
|
console.log('Handling command')
|
||||||
|
if (command === 'Improve') {
|
||||||
|
improveTextHandler(openAI)
|
||||||
|
} else if (command === 'Complete') {
|
||||||
|
completeTextHandler(openAI)
|
||||||
|
} else if (command === 'Ask') {
|
||||||
|
askHandler(openAI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(
|
||||||
|
function (request, sender, sendResponse) {
|
||||||
|
console.log(`Received request: ${JSON.stringify(request)}`)
|
||||||
|
if (request.command === 'setup') {
|
||||||
|
setAPIKey(request.apiKey)
|
||||||
|
} else {
|
||||||
|
if (!openAI) {
|
||||||
|
chrome.storage.local.get('openAIAPIKey').then(({ openAIAPIKey }) => {
|
||||||
|
setAPIKey(openAIAPIKey)
|
||||||
|
if (openAI) {
|
||||||
|
handleCommand(request.command)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleCommand(request.command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
2
scripts/jquery.js
vendored
Normal file
2
scripts/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
32
scripts/service-worker.js
Normal file
32
scripts/service-worker.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
async function sendMessage(message) {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
||||||
|
if (tab == null || tab.url?.startsWith('chrome://')) return undefined
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, message)
|
||||||
|
// do something with response here, not outside the function
|
||||||
|
// console.log(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListener(commandName) {
|
||||||
|
chrome.commands.onCommand.addListener((command) => {
|
||||||
|
if (command !== commandName) return
|
||||||
|
console.log(`Command ${command} triggered`)
|
||||||
|
sendMessage({ command: command })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user