diff --git a/README.md b/README.md index e0cce19..ea9771f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://openai.com/pricing > However, being orientated with managing files this project defaults to the 16k context with GPT-3.5-turbo-16k -If you wish to change the model, you must change the model in the extension.js file +If you wish to change the model, you must change the model in the `./src/gptContext.js` file # Examples @@ -39,6 +39,8 @@ Other features: ![Copy button demonstration](./images/copy-image.png) - Click copy to to copy the snippet of code into your file for fast coding +![Copy individual code](./images/demo-copying-quotes.gif) + # How it works We can select two files we want to pass through, however we can uncheck one of them for later debugging and enter our question: diff --git a/images/demo-copying-quotes.gif b/images/demo-copying-quotes.gif new file mode 100644 index 0000000..8e8e60d Binary files /dev/null and b/images/demo-copying-quotes.gif differ diff --git a/package.json b/package.json index 079acbb..16e7a54 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "gpt-contextfiles", "displayName": "GPT-ContextFiles", "description": "Choose the files to pass into GPT to provide a question with multiple files", - "version": "0.2.1", + "version": "0.2.2", "engines": { "vscode": "^1.79.0" }, @@ -39,7 +39,7 @@ "onCommand:extension.openGPTContextPanel", "onCommand:extension.gpt-context-sidebar" ], - "main": "./extension.js", + "main": "./src/extension.js", "contributes": { "commands": [ { diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..7423eda --- /dev/null +++ b/src/commands.js @@ -0,0 +1,84 @@ +const vscode = require('vscode'); +const { selectedFiles, fileDataProvider, handleQuestionSubmission } = require('./gptContext'); +const FileItem = require('./fileItem'); +const { getWebviewContent } = require('./webviewPanel'); + +const addFilesCommand = vscode.commands.registerCommand('extension.addFilesToGPTContext', () => { + const editor = vscode.window.activeTextEditor; + if (editor) { + const uri = editor.document.uri; + const existingFileIndex = selectedFiles.findIndex(file => file.uri.fsPath === uri.fsPath); + + if (existingFileIndex !== -1) { + // File already exists, remove it from the list + selectedFiles.splice(existingFileIndex, 1); + } else { + // Add the file to the list with selected state + selectedFiles.push(new FileItem(uri, true)); + } + + fileDataProvider.refresh(); + } +}); + +const openGPTContextPanelCommand = vscode.commands.registerCommand('extension.openGPTContextPanel', () => { + const panel = vscode.window.createWebviewPanel( + 'gptContextPanel', + 'GPT Context', + vscode.ViewColumn.One, + { + enableScripts: true + } + ); + + panel.webview.html = getWebviewContent(); + + panel.webview.onDidReceiveMessage(async message => { + if (message.command === 'submitQuestion') { + await handleQuestionSubmission(panel, message.text, message.selectedUris); + } else if (message.command === 'toggleFileSelection') { + const uri = message.uri; + const file = selectedFiles.find(file => file.uri.fsPath === uri); + if (file) { + file.toggleSelected(); + fileDataProvider.refresh(); + } + } else if (message.command === 'clearSelectedFiles') { + const clearedFiles = selectedFiles.filter(file => file.selected === false); + selectedFiles.length = 0; // Clear the array + clearedFiles.forEach(file => { + fileDataProvider.refresh(); + }); + panel.webview.html = getWebviewContent(); + } else if (message.command === 'refreshFiles') { + fileDataProvider.refresh(); + panel.webview.html = getWebviewContent(); + } + }); +}); + +const refreshSelectedFilesCommand = vscode.commands.registerCommand('extension.refreshSelectedFiles', () => { + fileDataProvider.refresh(); +}); + +// Command for clearing the selected files +const clearSelectedFilesCommand = vscode.commands.registerCommand('extension.clearSelectedFiles', () => { + selectedFiles.forEach(file => { + file.selected = false; + }); + fileDataProvider.refresh(); +}); + +// Command for refreshing all files +const refreshFilesCommand = vscode.commands.registerCommand('extension.refreshFiles', () => { + fileDataProvider.refresh(); +}); + + +module.exports = { + addFilesCommand, + openGPTContextPanelCommand, + refreshSelectedFilesCommand, + clearSelectedFilesCommand, + refreshFilesCommand +}; \ No newline at end of file diff --git a/src/extension.js b/src/extension.js new file mode 100644 index 0000000..15fc842 --- /dev/null +++ b/src/extension.js @@ -0,0 +1,52 @@ +const vscode = require('vscode'); +const { addFilesCommand, openGPTContextPanelCommand, refreshSelectedFilesCommand, clearSelectedFilesCommand, refreshFilesCommand } = require('./commands'); +const { getWebviewContent } = require('./webviewPanel'); +const { selectedFiles, fileDataProvider, handleQuestionSubmission } = require('./gptContext'); + +function activate(context) { + context.subscriptions.push(addFilesCommand); + context.subscriptions.push(openGPTContextPanelCommand); + context.subscriptions.push(refreshSelectedFilesCommand); + context.subscriptions.push(clearSelectedFilesCommand); + context.subscriptions.push(refreshFilesCommand); + vscode.window.registerTreeDataProvider('selectedFiles', fileDataProvider); + + const provider = { + resolveWebviewView(webviewView) { + webviewView.webview.options = { + enableScripts: true + }; + webviewView.webview.html = getWebviewContent(); + webviewView.webview.onDidReceiveMessage(async message => { + if (message.command === 'toggleFileSelection') { + const uri = message.uri; + const file = selectedFiles.find(file => file.uri.fsPath === uri); + if (file) { + file.toggleSelected(); + fileDataProvider.refresh(); + } + } else if (message.command === 'clearSelectedFiles') { + const clearedFiles = selectedFiles.filter(file => file.selected === false); + selectedFiles.length = 0; // Clear the array + clearedFiles.forEach(file => { + fileDataProvider.refresh(); + }); + webviewView.webview.html = getWebviewContent(); + } else if (message.command === 'refreshFiles') { + fileDataProvider.refresh(); + webviewView.webview.html = getWebviewContent(); + } else if (message.command === 'submitQuestion') { + await handleQuestionSubmission(webviewView, message.text, message.selectedUris); + } else if (message.command === 'codeCopied') { + vscode.window.showInformationMessage('Code copied to clipboard'); + } + }); + } + }; + + context.subscriptions.push(vscode.window.registerWebviewViewProvider('gpt-context-sidebar', provider)); + +} + + +exports.activate = activate; \ No newline at end of file diff --git a/src/fileDataProvider.js b/src/fileDataProvider.js new file mode 100644 index 0000000..2823080 --- /dev/null +++ b/src/fileDataProvider.js @@ -0,0 +1,38 @@ +const vscode = require('vscode'); +const FileItem = require('./fileItem.js'); + +// Represents the selected files in the file explorer +const selectedFiles = []; + +// Tree data provider for the selected files +class FileDataProvider { + constructor() { + this._onDidChangeTreeData = new vscode.EventEmitter(); + this.onDidChangeTreeData = this._onDidChangeTreeData.event; + } + + refresh() { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element) { + return { + label: element.uri.fsPath, + collapsibleState: vscode.TreeItemCollapsibleState.None + }; + } + + getChildren(element) { + if (element) { + return []; + } + + // Return only the selected files + return selectedFiles.filter(file => file.selected); + } +} + +module.exports = { + FileDataProvider, + selectedFiles +}; diff --git a/src/fileItem.js b/src/fileItem.js new file mode 100644 index 0000000..66b0d4f --- /dev/null +++ b/src/fileItem.js @@ -0,0 +1,12 @@ +class FileItem { + constructor(uri, selected = false) { + this.uri = uri; + this.selected = selected; + } + + toggleSelected() { + this.selected = !this.selected; + } +} + +module.exports = FileItem; diff --git a/src/gptContext.js b/src/gptContext.js new file mode 100644 index 0000000..e89a76c --- /dev/null +++ b/src/gptContext.js @@ -0,0 +1,63 @@ +const vscode = require('vscode'); +const { Configuration, OpenAIApi } = require("openai"); +const FileDataProvider = require('./fileDataProvider'); +const { getWebviewContent } = require('./webviewPanel'); + + +const configuration = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const openai = new OpenAIApi(configuration); + +const selectedFiles = FileDataProvider.selectedFiles; +const fileDataProvider = new FileDataProvider.FileDataProvider(); + +async function handleQuestionSubmission(panel, question, selectedUris) { + // Update the selectedFiles array based on the selectedUris + selectedFiles.forEach(file => { + file.selected = selectedUris.includes(file.uri.fsPath); + }); + + fileDataProvider.refresh(); + + const fileContents = selectedFiles + .filter(file => file.selected) + .map(file => { + const document = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === file.uri.fsPath); + if (document) { + const lines = document.getText().split('\n'); + const formattedLines = lines.map(line => `\t${line}`).join('\n'); + return `${file.uri.fsPath}:\n\`\`\`\n${formattedLines}\n\`\`\``; + } + return ''; + }) + .join('\n\n'); + + // Call OpenAI API with the question and file contents + try { + const chatCompletion = await openai.createChatCompletion({ + model: "gpt-3.5-turbo-16k", + messages: [ + { role: "system", content: "Answer the coding questions, only provide the code and documentation, explaining the solution after providing the code." }, + { role: "user", content: question + "\n" + fileContents}, + ], + }); + + // Extract the answer from the OpenAI response + const answer = chatCompletion.data.choices[0].message.content; + + // Update the webview content to display only the OpenAI response + panel.webview.html = getWebviewContent(answer, question); + } catch (error) { + // Handle any errors from the OpenAI API + console.error("Failed to get OpenAI response:", error); + panel.webview.html = getWebviewContent(`Failed to get response from OpenAI API. Error: ${error.message}`, question); + } +} + +module.exports = { + handleQuestionSubmission, + fileDataProvider, + selectedFiles +}; \ No newline at end of file diff --git a/extension.js b/src/webviewPanel.js similarity index 59% rename from extension.js rename to src/webviewPanel.js index fce9444..c20db73 100644 --- a/extension.js +++ b/src/webviewPanel.js @@ -1,179 +1,6 @@ const vscode = require('vscode'); -const { Configuration, OpenAIApi } = require("openai"); +const { selectedFiles } = require('./fileDataProvider'); -// move these into the script so that instead of echoing the question and the contents, -// it will echo the question, followed by the answer from the response when the submit button is pressed. -const configuration = new Configuration({ - apiKey: process.env.OPENAI_API_KEY, -}); - -const openai = new OpenAIApi(configuration); - -// Represents a file item in the file explorer -class FileItem { - constructor(uri, selected = false) { - this.uri = uri; - this.selected = selected; - } - - toggleSelected() { - this.selected = !this.selected; - } -} - -// Represents the selected files in the file explorer -const selectedFiles = []; - -// Tree data provider for the selected files -class FileDataProvider { - constructor() { - this._onDidChangeTreeData = new vscode.EventEmitter(); - this.onDidChangeTreeData = this._onDidChangeTreeData.event; - } - - refresh() { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element) { - return { - label: element.uri.fsPath, - collapsibleState: vscode.TreeItemCollapsibleState.None - }; - } - - getChildren(element) { - if (element) { - return []; - } - - // Return only the selected files - return selectedFiles.filter(file => file.selected); - } -} - -// Command for adding files to gpt-contextfiles -const addFilesCommand = vscode.commands.registerCommand('extension.addFilesToGPTContext', () => { - const editor = vscode.window.activeTextEditor; - if (editor) { - const uri = editor.document.uri; - const existingFileIndex = selectedFiles.findIndex(file => file.uri.fsPath === uri.fsPath); - - if (existingFileIndex !== -1) { - // File already exists, remove it from the list - selectedFiles.splice(existingFileIndex, 1); - } else { - // Add the file to the list with selected state - selectedFiles.push(new FileItem(uri, true)); - } - - fileDataProvider.refresh(); - } -}); - -const fileDataProvider = new FileDataProvider(); - -// Function to handle question submission -async function handleQuestionSubmission(panel, question, selectedUris) { - // Update the selectedFiles array based on the selectedUris - selectedFiles.forEach(file => { - file.selected = selectedUris.includes(file.uri.fsPath); - }); - - fileDataProvider.refresh(); - - const fileContents = selectedFiles - .filter(file => file.selected) - .map(file => { - const document = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === file.uri.fsPath); - if (document) { - const lines = document.getText().split('\n'); - const formattedLines = lines.map(line => `\t${line}`).join('\n'); - return `${file.uri.fsPath}:\n\`\`\`\n${formattedLines}\n\`\`\``; - } - return ''; - }) - .join('\n\n'); - - // Call OpenAI API with the question and file contents - try { - const chatCompletion = await openai.createChatCompletion({ - model: "gpt-3.5-turbo-16k", - messages: [ - { role: "system", content: "Answer the coding questions, only provide the code and documentation, explaining the solution after providing the code." }, - { role: "user", content: question + "\n" + fileContents}, - ], - }); - - // Extract the answer from the OpenAI response - const answer = chatCompletion.data.choices[0].message.content; - - // Update the webview content to display only the OpenAI response - panel.webview.html = getWebviewContent(answer, question); - } catch (error) { - // Handle any errors from the OpenAI API - console.error("Failed to get OpenAI response:", error); - panel.webview.html = getWebviewContent(`Failed to get response from OpenAI API. Error: ${error.message}`, question); - } -} - -// Command for displaying the webview panel -const openGPTContextPanelCommand = vscode.commands.registerCommand('extension.openGPTContextPanel', () => { - const panel = vscode.window.createWebviewPanel( - 'gptContextPanel', - 'GPT Context', - vscode.ViewColumn.One, - { - enableScripts: true - } - ); - - panel.webview.html = getWebviewContent(); - - panel.webview.onDidReceiveMessage(async message => { - if (message.command === 'submitQuestion') { - await handleQuestionSubmission(panel, message.text, message.selectedUris); - } else if (message.command === 'toggleFileSelection') { - const uri = message.uri; - const file = selectedFiles.find(file => file.uri.fsPath === uri); - if (file) { - file.toggleSelected(); - fileDataProvider.refresh(); - } - } else if (message.command === 'clearSelectedFiles') { - const clearedFiles = selectedFiles.filter(file => file.selected === false); - selectedFiles.length = 0; // Clear the array - clearedFiles.forEach(file => { - fileDataProvider.refresh(); - }); - panel.webview.html = getWebviewContent(); - } else if (message.command === 'refreshFiles') { - fileDataProvider.refresh(); - panel.webview.html = getWebviewContent(); - } - }); -}); - - -// Command for refreshing the selected files -const refreshSelectedFilesCommand = vscode.commands.registerCommand('extension.refreshSelectedFiles', () => { - fileDataProvider.refresh(); -}); - -// Command for clearing the selected files -const clearSelectedFilesCommand = vscode.commands.registerCommand('extension.clearSelectedFiles', () => { - selectedFiles.forEach(file => { - file.selected = false; - }); - fileDataProvider.refresh(); -}); - -// Command for refreshing all files -const refreshFilesCommand = vscode.commands.registerCommand('extension.refreshFiles', () => { - fileDataProvider.refresh(); -}); - -// Helper function to generate the HTML content for the webview panel function getWebviewContent(apiResponse = '', question = '') { const fileList = selectedFiles .map( @@ -200,20 +27,20 @@ function getWebviewContent(apiResponse = '', question = '') { color: #d4d4d4; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } - + .textbox { width: 100%; height: 200px; resize: both; padding: 10px; } - + .buttons { display: flex; justify-content: space-between; width: 50%; } - + .button { flex-grow: 1; background-color: #007acc; @@ -225,28 +52,30 @@ function getWebviewContent(apiResponse = '', question = '') { outline: none; font-size: 14px; } - + #response { white-space: pre-wrap; + background: #343434; + border-radius: 5px; + padding: 10px; } - + #file-list { margin-top: 20px; - border } - + .form-group { display: flex; flex-direction: column; margin-bottom: 20px; width: 100%; } - + .form-group label { margin-bottom: 5px; font-size: 14px; } - + .form-group input[type="text"] { padding: 10px; font-size: 14px; @@ -255,17 +84,17 @@ function getWebviewContent(apiResponse = '', question = '') { background-color: #2d2d2d; color: #d4d4d4; } - + .form-group input[type="text"]::placeholder { color: #d4d4d4; } - + .form-group .button-options { display: flex; justify-content: space-between; margin-top: 10px; } - + .form-group .button-options button { padding: 10px; font-size: 14px; @@ -275,107 +104,107 @@ function getWebviewContent(apiResponse = '', question = '') { background-color: #007acc; color: #fff; } - + .form-group .button-options button:hover { background-color: #005f8c; } - + .form-group .button-options button:active { background-color: #004d73; } - + .form-group .button-options button:focus { box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.5); } - + .file-list { margin-top: 20px; width: 100%; } - + .file-list h2 { margin-bottom: 10px; font-size: 14px; } - + .file-list .file-item { display: flex; align-items: center; margin-bottom: 5px; font-size: 14px; } - + .file-list .file-item input[type="checkbox"] { margin-right: 5px; } - + .file-list .file-item label { margin-bottom: 0; } - + .file-list .file-item .file-path { overflow-wrap: break-word; } - + .file-list .file-item .file-path:hover { text-decoration: underline; cursor: pointer; } - + .file-list .file-item .file-path:active { color: #007acc; } - + .file-list .file-item .file-path:focus { box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.5); } - + .collapsible { background-color: #2d2d2d; color: #d4d4d4; cursor: pointer; padding: 10px; - width: 100%; + width: 98%; border: none; outline: none; text-align: left; font-size: 14px; } - + .collapsible:hover { background-color: #3c3c3c; } - + .collapsible:active { background-color: #4c4c4c; } - + .collapsible:focus { box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.5); } - + .content { padding: 0 10px; display: none; overflow: hidden; background-color: #f1f1f1; - width: 100%; + width: 98%; } - + .content p { - margin-top: 0; font-size: 14px; + margin: 0; } - + .active, .collapsible:hover { background-color: #555; } - + .active:after { content: "\\2212"; } - + .collapsible:after { content: "\\002B"; color: #d4d4d4; @@ -383,11 +212,11 @@ function getWebviewContent(apiResponse = '', question = '') { float: right; margin-left: 5px; } - + .active:after { content: "\\2212"; } - + .collapsible:after { content: "\\002B"; color: #d4d4d4; @@ -395,7 +224,7 @@ function getWebviewContent(apiResponse = '', question = '') { float: right; margin-left: 5px; } - + #rendered { background-color: #2d2d2d; word-wrap: wrap; @@ -403,23 +232,25 @@ function getWebviewContent(apiResponse = '', question = '') { border: 1px solid white; border-radius: 5px; padding: 10px; + color: white; } - + #question-rep { font-weight: bold; background-color: #2d2d2d; word-wrap: wrap; border: 1px solid white; border-radius: 5px; + padding: 10px; + margin-top: 20px; } - + div#api-response.content.active { background-color: #313131; } - + #code-block { padding: 10px 0 10px 10px; - padding- border-radius: 5px; background-color: black; border: none; @@ -427,12 +258,12 @@ function getWebviewContent(apiResponse = '', question = '') { color: inherit; cursor: pointer; outline: inherit; - width: 100%; + width: 98%; text-align: left; display: inline-block; position: relative; } - + #copy-button { padding: 5px; border-radius: 5px; @@ -447,12 +278,12 @@ function getWebviewContent(apiResponse = '', question = '') { top: 0; right: 0; } - + #copy-button:hover { background-color: #fff; color: #000; } - + @@ -584,53 +415,6 @@ function getWebviewContent(apiResponse = '', question = '') { `; } - - -// Activates the extension -function activate(context) { - context.subscriptions.push(addFilesCommand); - context.subscriptions.push(openGPTContextPanelCommand); - context.subscriptions.push(refreshSelectedFilesCommand); - context.subscriptions.push(clearSelectedFilesCommand); - context.subscriptions.push(refreshFilesCommand); - vscode.window.registerTreeDataProvider('selectedFiles', fileDataProvider); - - const provider = { - resolveWebviewView(webviewView) { - webviewView.webview.options = { - enableScripts: true - }; - webviewView.webview.html = getWebviewContent(); - webviewView.webview.onDidReceiveMessage(async message => { - if (message.command === 'toggleFileSelection') { - const uri = message.uri; - const file = selectedFiles.find(file => file.uri.fsPath === uri); - if (file) { - file.toggleSelected(); - fileDataProvider.refresh(); - } - } else if (message.command === 'clearSelectedFiles') { - const clearedFiles = selectedFiles.filter(file => file.selected === false); - selectedFiles.length = 0; // Clear the array - clearedFiles.forEach(file => { - fileDataProvider.refresh(); - }); - webviewView.webview.html = getWebviewContent(); - } else if (message.command === 'refreshFiles') { - fileDataProvider.refresh(); - webviewView.webview.html = getWebviewContent(); - } else if (message.command === 'submitQuestion') { - await handleQuestionSubmission(webviewView, message.text, message.selectedUris); - } else if (message.command === 'codeCopied') { - vscode.window.showInformationMessage('Code copied to clipboard'); - } - }); - } - }; - - context.subscriptions.push(vscode.window.registerWebviewViewProvider('gpt-context-sidebar', provider)); - -} - - -exports.activate = activate; +module.exports = { + getWebviewContent +}; \ No newline at end of file