-
Notifications
You must be signed in to change notification settings - Fork 9
feat: copilot chat with custom questions #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <div id="copilot-side-panel"> | ||
| <button id="btn-copilot-close" class="btn">Close</button> | ||
| <h5>Ask Copilot</h5> | ||
| <textarea id="copilot-gpt-question"></textarea> | ||
| <button id="btn-copilot-ask">Ask</button> | ||
| <div style="display: none"> | ||
| <h5>GPT Response</h5> | ||
| <div style="margin: 10px 0"> | ||
| <textarea id="copilot-gpt-response"></textarea> | ||
| </div> | ||
| <button id="btn-copilot-replace">Replace</button> | ||
| <button id="btn-copilot-regenerate" style="float: right">Regenerate</button> | ||
| </div> | ||
| </div> | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ function debounce<T extends () => 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( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: let's use "insert", since it's not always adding content to the end of the document. |
||
| 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); | ||
| setupKeydownListener(10); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this prompt seems a bit unclear to me, would it be more intuitive to ask GPT to answer the question based on the selection?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue is that the reply is often: "Sure thing! In order to solve xxxx you write:" Where the answer includes a human response and explanation instead of simply giving the answer
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hence why the prompt is forced to only provide the output
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part makes sense to me. I'm a bit confused about how the user is expected to use this feature. I think when a user selects some text, they may want to:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I aim to get an improvement/latex as a response rather than an explanation. When selecting text it aims to answer the question with the selection as context. When not selecting text it just tries to answer the question with a response that can be inserted into the paper. So your 1. point is correct. Answering questions about a selection is not my aim with this PR. Github copilot solves this ambiguity using commands such as /explain and /fix that indicate what you want to do with a selection. Future changes could incorporate something similar.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... Or we could add a "improve" and "explain" button? |
||
| `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 ( | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no content from the paper is included in the prompt right?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, currently it only includes the question
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this expected? GPT doesn't have the context to continue the "academic paper" in the Overleaf editor.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes and no. In my examples I usually selected something and asked a question to modify that selected text. Or I asked for non-contextual question such as "create a table to do this or that" which did not require input about the paper. But I agree it could be useful to add context. |
||
| `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}` | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
tabs?[0]?.id