diff --git a/README.md b/README.md
index 39945a5..cfbdb66 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ A Copilot browser extension for the Overleaf website.
- Auto-Completion
- Text Enhancement with GPT
- Discover Related Content on arXiv
+- Ask specific questions to GPT
---
diff --git a/config/webpack.config.js b/config/webpack.config.js
index c9bf184..4a87f61 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -22,6 +22,7 @@ const config = (env, argv) =>
https: false,
url: false,
timers: false,
+ string_decoder: false
},
},
});
diff --git a/package-lock.json b/package-lock.json
index b6bb2a6..01bb485 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "overleaf-copilot",
- "version": "0.1.4",
+ "version": "0.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "overleaf-copilot",
- "version": "0.1.4",
+ "version": "0.1.8",
"dependencies": {
"@types/crypto-js": "^4.1.2",
"@types/node": "^20.6.2",
diff --git a/public/chatPanel.html b/public/chatPanel.html
new file mode 100644
index 0000000..887663c
--- /dev/null
+++ b/public/chatPanel.html
@@ -0,0 +1,15 @@
+
+
+
Ask Copilot
+
+
+
+
GPT Response
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/manifest.json b/public/manifest.json
index 0451c45..1335b27 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Overleaf Copilot",
- "version": "0.1.8",
+ "version": "0.1.9",
"description": "Copilot for Overleaf",
"icons": {
"16": "icons/icon_16.png",
@@ -14,6 +14,7 @@
"resources": [
"icons/icon_128.png",
"sidePanel.html",
+ "chatPanel.html",
"search.html"
],
"matches": [
@@ -29,7 +30,9 @@
"default_popup": "popup.html"
},
"permissions": [
- "storage"
+ "storage",
+ "scripting",
+ "activeTab"
],
"content_scripts": [
{
@@ -54,5 +57,14 @@
"contentIsoScript.css"
]
}
- ]
+ ],
+ "commands": {
+ "chat": {
+ "suggested_key": {
+ "default": "Ctrl+Shift+I",
+ "mac": "Command+Shift+I"
+ },
+ "description": "Prompt user to chat with Copilot"
+ }
+ }
}
\ No newline at end of file
diff --git a/src/background.ts b/src/background.ts
index d619294..be01d43 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -10,3 +10,13 @@ chrome.runtime.onMessage.addListener(function (request) {
});
}
});
+
+chrome.commands.onCommand.addListener((command) => {
+ if (command === "chat") {
+ chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
+ if (tabs && tabs[0] && tabs[0].id) {
+ return chrome.tabs.sendMessage(tabs[0].id, { type: "chat:open" });
+ }
+ });
+ }
+});
diff --git a/src/chatPanel.ts b/src/chatPanel.ts
new file mode 100644
index 0000000..e0a08b6
--- /dev/null
+++ b/src/chatPanel.ts
@@ -0,0 +1,101 @@
+'use strict';
+
+import { EditorContent } from "./types";
+import { AskQuestion } from "./utils/chat";
+
+export async function LoadChatPanel(
+ selection?: string,
+ from?: number,
+ to?: number
+) {
+ const rightContainer = document.querySelector(
+ '.ide-react-panel[data-panel-id="panel-pdf"]'
+ ) as HTMLElement;
+ if (!rightContainer) return;
+
+ const sidePanel = document.createElement('div');
+ const sidePanelUrl = chrome.runtime.getURL('chatPanel.html');
+ sidePanel.innerHTML = await (await fetch(sidePanelUrl)).text();
+
+ const questionTxt = sidePanel.querySelector(
+ '#copilot-gpt-question'
+ ) as HTMLInputElement;
+
+ const askBtn = sidePanel.querySelector(
+ '#btn-copilot-ask'
+ ) as HTMLButtonElement;
+ askBtn.onclick = async () => {
+ askBtn.disabled = true;
+ await generate(questionTxt.value, selection, sidePanel);
+ askBtn.disabled = false;
+ };
+
+ const regenerateBtn = sidePanel.querySelector(
+ '#btn-copilot-regenerate'
+ ) as HTMLButtonElement;
+ regenerateBtn.onclick = async () => {
+ regenerateBtn.disabled = true;
+ await generate(questionTxt.value, selection, sidePanel);
+ regenerateBtn.disabled = false;
+ };
+
+ const replaceBtn = sidePanel.querySelector(
+ '#btn-copilot-replace'
+ ) as HTMLButtonElement;
+ if (!selection) {
+ // Replace is "insert"
+ replaceBtn.textContent = 'Insert';
+ }
+
+ replaceBtn.onclick = () => {
+ const responseTxt = (
+ sidePanel.querySelector('#copilot-gpt-response') as HTMLInputElement
+ ).value;
+
+ if (selection) {
+ window.dispatchEvent(
+ new CustomEvent('copilot:editor:replace', {
+ detail: {
+ improvement: responseTxt,
+ from: from,
+ to: to,
+ },
+ })
+ );
+ } else {
+ window.dispatchEvent(
+ new CustomEvent('copilot:editor:append', {
+ detail: {
+ content: responseTxt,
+ },
+ })
+ );
+ }
+ document.getElementById('copilot-side-panel')?.remove();
+ };
+
+ const closeBtn = sidePanel.querySelector(
+ '#btn-copilot-close'
+ ) as HTMLButtonElement;
+ closeBtn.onclick = () =>
+ document.getElementById('copilot-side-panel')?.remove();
+ rightContainer.appendChild(sidePanel);
+
+ // Focus on the question input
+ questionTxt.focus();
+
+ return;
+}
+
+async function generate(question: string, selection: string | undefined, sidePanel: HTMLElement) {
+ const textarea = sidePanel.querySelector(
+ '#copilot-gpt-response'
+ ) as HTMLInputElement;
+ textarea.disabled = true;
+ const improvement = await AskQuestion(question, selection);
+ textarea.value = improvement;
+ textarea.parentElement!.parentElement!.style.display = 'block';
+ textarea.style.height = 'auto';
+ textarea.style.height = `${Math.min(textarea.scrollHeight + 2, 200)}px`;
+ textarea.disabled = false;
+}
diff --git a/src/contentIsoScript.css b/src/contentIsoScript.css
index 678216f..d0f61c0 100644
--- a/src/contentIsoScript.css
+++ b/src/contentIsoScript.css
@@ -41,12 +41,13 @@ p#copilot-original-content {
border-radius: 2px;
}
-textarea#copilot-gpt-response {
+textarea#copilot-gpt-response, textarea#copilot-gpt-question {
resize: none;
width: 100%;
padding: 5px 10px;
}
+
button#btn-copilot-close {
float: right;
margin-top: 7px;
diff --git a/src/contentIsoScript.ts b/src/contentIsoScript.ts
index 256ddca..f707014 100644
--- a/src/contentIsoScript.ts
+++ b/src/contentIsoScript.ts
@@ -9,8 +9,14 @@ import {
CONFIG_DISABLE_IMPROVEMENT,
CONFIG_MAX_PROMPT_WORDS,
} from './constants';
+import { LoadChatPanel } from './chatPanel';
let cursorPos: { row: number; column: number } | null = null;
+let currentSelection: {
+ selection?: string;
+ from?: number;
+ to?: number;
+} = {};
function debounce<
T extends (
@@ -52,6 +58,9 @@ async function onEditorUpdate(
col: number,
signal: AbortSignal
) {
+ // Delete selection
+ currentSelection = {};
+
const config = await chrome.storage.local.get([CONFIG_DISABLE_COMPLETION]);
if (!!config[CONFIG_DISABLE_COMPLETION]) return;
@@ -110,6 +119,9 @@ async function onEditorSelect(
head: number;
}>
) {
+ // Save current selection
+ currentSelection = event.detail;
+
const config = await chrome.storage.local.get([CONFIG_DISABLE_IMPROVEMENT]);
if (!!config[CONFIG_DISABLE_IMPROVEMENT]) return;
@@ -165,4 +177,11 @@ window.addEventListener('load', onConfigUpdate);
chrome.runtime.onMessage.addListener(async function (request) {
if (request.type === 'config:update') await onConfigUpdate();
+ if (request.type === 'chat:open') {
+
+ await LoadChatPanel(
+ currentSelection.selection,
+ currentSelection.from,
+ currentSelection.to)
+ };
});
diff --git a/src/contentMainScript.ts b/src/contentMainScript.ts
index ff6816c..c4529b6 100644
--- a/src/contentMainScript.ts
+++ b/src/contentMainScript.ts
@@ -12,6 +12,7 @@ function debounce void>(func: T): () => void {
document.getElementById('copilot-sidebar-button')?.remove();
document.getElementById('copilot-suggestion')?.remove();
document.getElementById('copilot-side-panel')?.remove();
+ document.getElementById('copilot-chat-panel')?.remove();
if (timeout) clearTimeout(timeout);
@@ -104,6 +105,17 @@ function onConfigUpdate(e: CustomEvent<{ [CONFIG_MAX_PROMPT_WORDS]: number }>) {
maxPromptWords = e.detail[CONFIG_MAX_PROMPT_WORDS] || maxPromptWords;
}
+function onEditorAppend(
+ e: CustomEvent<{ content: string }>
+) {
+ var editor = document.querySelector('.cm-content');
+ if (!editor) return;
+ const content = editor as any as EditorContent;
+ const currentPos = content.cmView.view.state.selection.main.head;
+ const changes = { from: currentPos, to: currentPos, insert: e.detail.content };
+ content.cmView.view.dispatch({ changes });
+}
+
function onAcceptImprovement(
e: CustomEvent<{ improvement: string; from: number; to: number }>
) {
@@ -130,6 +142,7 @@ window.addEventListener(
onAcceptImprovement as EventListener
);
window.addEventListener('cursor:editor:update', debounce(onCursorUpdate));
+window.addEventListener('copilot:editor:append', onEditorAppend as EventListener);
window.addEventListener(
'copilot:config:update',
onConfigUpdate as EventListener
@@ -147,4 +160,4 @@ const setupKeydownListener = (n: number) => {
return true;
};
-setupKeydownListener(10);
\ No newline at end of file
+setupKeydownListener(10);
diff --git a/src/utils/chat.ts b/src/utils/chat.ts
new file mode 100644
index 0000000..d15103f
--- /dev/null
+++ b/src/utils/chat.ts
@@ -0,0 +1,58 @@
+'use strict';
+
+import OpenAI from 'openai';
+import {
+ CONFIG_API_KEY,
+ CONFIG_BASE_URL,
+ CONFIG_IMPROVEMENT_CUSTOM_PROMPT,
+ CONFIG_MODEL,
+ DEFAULT_MODEL,
+} from '../constants';
+
+export async function AskQuestion(question: string, selection?: string) {
+ const config = await chrome.storage.local.get([
+ CONFIG_API_KEY,
+ CONFIG_BASE_URL,
+ CONFIG_MODEL,
+ CONFIG_IMPROVEMENT_CUSTOM_PROMPT,
+ ]);
+
+ if (!config[CONFIG_API_KEY]) return '';
+
+ const openai = new OpenAI({
+ apiKey: config[CONFIG_API_KEY],
+ baseURL: config[CONFIG_BASE_URL] || undefined,
+ dangerouslyAllowBrowser: true,
+ });
+
+ const completion = await openai.chat.completions.create({
+ messages: [
+ {
+ role: 'user',
+ content: buildAskPrompt(
+ question,
+ selection
+ ),
+ },
+ ],
+ model: config[CONFIG_MODEL] || DEFAULT_MODEL,
+ });
+
+ return completion.choices[0].message.content?.trim() ?? '';
+}
+
+function buildAskPrompt(question: string, selection?: string) {
+ if (selection) {
+ return (
+ `Rewrite and improve the following content:\n` + `${selection}\n\n` +
+ `Making sure to maintain semantic continuity, the content should adhere to the following question:\n` +
+ `${question}`
+ );
+ } else {
+ return (
+ `Continue the academic paper in LaTeX below by providing a solution to the following question.\n` +
+ `Only provide the output that has to be written to answer the question:\n\n` +
+ `${question}`
+ );
+ }
+}