├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── applyChange.js ├── background.js ├── content.js ├── contextMenu.js ├── easycode.png ├── fileChips.js ├── files.js ├── interceptSubmit.js ├── manifest.json ├── mixpanel.js ├── options.html ├── options.js ├── platform.js ├── styles.css └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Byte-compiled files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Virtual environment directories 9 | venv/ 10 | env/ 11 | 12 | # Distribution and build directories 13 | build/ 14 | dist/ 15 | 16 | 17 | .aider* 18 | .env 19 | easycode.ignore 20 | 21 | # ignore python folder 22 | python 23 | reference.md 24 | testA.js 25 | 26 | .DS_Store 27 | *.zip 28 | mixpanel-chrome.js 29 | options.backup.js -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome", 11 | "url": "https://www.chatgpt.com", 12 | "timeout": 20000, 13 | "runtimeArgs": ["--load-extension=${workspaceFolder}"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 EasyCode-AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


Codaware - by EasyCode

2 | 3 | ## Chat with Codebase from ChatGPT or Claude 4 | 5 | [![Codaware Demo](https://img.youtube.com/vi/WFo2guwa8bY/maxresdefault.jpg)](https://youtu.be/WFo2guwa8bY) 6 | 7 | - no more copying & pasting 8 | - don't pay for multiple subscriptions, use chatgpt plus or claude paid plan, and advanced models 9 | - take advantage of web features such as o1-preview, image, web search, artifacts, etc. 10 | 11 | ## Installation 12 | 13 | ### Option 1: Chrome Web Store (Recommended) 14 | You can install Codaware directly from the Chrome Web Store: 15 | [Click here to install Codaware](https://chromewebstore.google.com/detail/codaware-by-easycode/mmelffgafmcppjiecckdlmgbbdcgmkdg?hl=en) 16 | 17 | ### Option 2: Manual Installation (Load Unpacked) 18 | If you prefer to install manually: 19 | 1. Download the extension files 20 | 2. Open Chrome and navigate to `chrome://extensions/` 21 | 3. Enable "Developer mode" in the top right corner 22 | 4. Click "Load unpacked" 23 | 5. Select the downloaded extension directory 24 | 25 | ## Features 26 | - [x] reference files on ChatGPT & Claude.ai 27 | - [x] ability to apply changes directly from ChatGPT and Claude.ai 28 | 29 | ## Bugs 30 | - [x] hitting ENTER sends question without injecting file content [Medium] 31 | - [x] Error loading files sometimes 32 | - [x] socket error sometimes [Tiny] 33 | - [x] prevent duplicated files from being added. [Tiny] 34 | 35 | ## Improvements 36 | - [x] refactor front end into more modular components [Medium] 37 | - [ ] migrate project to react or nextjs [Medium] 38 | - [ ] add bundling 39 | - [x] add a file name place holder after the file is injected [tiny] 40 | - [x] refactor vscode side to its own folder and make it modular as well, 41 | - [x] stop generation doesn't work due to capturing the button submit [Medium?] 42 | - [ ] collapse the codeblocks in the "sent" sections [Tiny/Easy] 43 | - [ ] don't resend file content its already in chat context [Tiny] 44 | 45 | ## Future Feature Ideas 46 | - [ ] ability to @problems inside chrome extension 47 | - [ ] ability to @codebase and RAG the codebase 48 | - [ ] [WIP] add ability to drag a folder and parse the file path, and fetch the files.. [Medium] 49 | - [ ] send file updates from vscode to browser. 50 | - [ ] compare answer with different models such as DeepSeek, Qwen, Llama 3, etc. [Hard] 51 | - [ ] add ability to watch for errors in console, auto suggest it in the web browser [Medium?] 52 | 53 | ## Bugs or Feature Ideas 54 | - Please submit a issue. -------------------------------------------------------------------------------- /applyChange.js: -------------------------------------------------------------------------------- 1 | const SPINNER_SVG = ` 2 | 3 | 4 | `; 5 | 6 | const TIMEOUT_DURATION = 120000; // 2 minutes in milliseconds 7 | 8 | 9 | function findActionButtonContainerFromCodeBlock(codeBlock) { 10 | const platform = getCurrentPlatform(); 11 | 12 | if (platform === PLATFORMS.CLAUDE) { 13 | const codeBlockParent = codeBlock.parentElement; 14 | if (codeBlockParent.classList.contains('h-fit') && codeBlockParent.classList.contains('w-fit')) { 15 | // This is an artifact codeblock 16 | const parentContainer = codeBlock.closest('div.h-full.bg-bg-000'); 17 | const actionButtonContainer = parentContainer.querySelector('div.justify-end > button').parentElement; 18 | if (actionButtonContainer) { 19 | // console.log(actionButtonContainer); 20 | return actionButtonContainer; 21 | } else { 22 | console.log("no action button container found"); 23 | return null; 24 | } 25 | } else { 26 | // this is an in chat codeblock 27 | const parentContainer = codeBlock.closest('pre'); 28 | const actionButtonContainer = parentContainer.querySelector('button').parentElement.parentElement; 29 | if (actionButtonContainer) { 30 | //console.log(actionButtonContainer); 31 | return actionButtonContainer; 32 | } else { 33 | console.log("no action button container found"); 34 | return null; 35 | } 36 | } 37 | } else if (platform === PLATFORMS.CHATGPT) { 38 | if (codeBlock.tagName === 'CODE') { 39 | // this is a typical in chat code block 40 | const parentContainer = codeBlock.closest('pre'); 41 | const actionButtonContainer = parentContainer.querySelector('div.sticky'); 42 | if (actionButtonContainer) { 43 | // console.log(actionButtonContainer); 44 | return actionButtonContainer.firstChild; 45 | } else { 46 | console.log("no action button container found"); 47 | return null; 48 | } 49 | } else if (!Array.from(codeBlock.classList).some(className => className.includes('preview'))) { 50 | // this is a canvas code block in editor mode 51 | console.log("canvas code block in editor mode"); 52 | const parentContainer = codeBlock.closest('section'); 53 | const actionButtonContainer = parentContainer.querySelector('header'); 54 | if (actionButtonContainer) { 55 | console.log(actionButtonContainer); 56 | return actionButtonContainer.lastElementChild; 57 | } else { 58 | console.log("no action button container found"); 59 | return null; 60 | } 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | function addCodeBlockButton(codeBlock) { 67 | // console.log(codeBlock); 68 | 69 | // Check if button was already added using dataset 70 | if (codeBlock.dataset.buttonAdded) { 71 | console.log("skipping adding button, already has it"); 72 | return; // Add early return here 73 | } 74 | 75 | const platform = getCurrentPlatform(); 76 | if (!platform) return; 77 | const selectors = platform.selectors; 78 | 79 | // For both ChatGPT and Claude 80 | if (codeBlock.matches(selectors.codeBlock)) { 81 | const buttonContainer = findActionButtonContainerFromCodeBlock(codeBlock, platform.selectors.codeActionButtonContainer); 82 | 83 | if (!buttonContainer) { 84 | console.error('Could not find button container'); 85 | return; 86 | } 87 | 88 | //let buttonContainerElement = buttonContainer.querySelector(`${platform.buttonStyle.container}`); 89 | 90 | // Check if button already exists in the container 91 | if (buttonContainer) { 92 | const existingApplyButton = Array.from(buttonContainer.querySelectorAll('button')).find( 93 | btn => btn.innerHTML === platform.buttonStyle.icon 94 | ); 95 | if (existingApplyButton) { 96 | console.log("Apply button already exists, remove the current one"); 97 | codeBlock.dataset.buttonAdded = 'true'; 98 | buttonContainer.removeChild(existingApplyButton); 99 | } 100 | } else { 101 | buttonContainer = document.createElement('div'); 102 | buttonContainer.className = platform.buttonStyle.container; 103 | codeBlock.appendChild(buttonContainer); 104 | } 105 | 106 | const applyButton = document.createElement('button'); 107 | if (platform === PLATFORMS.CHATGPT) { 108 | applyButton.style.cssText = platform.buttonStyle.style; 109 | buttonContainer.prepend(applyButton); 110 | } else if (platform == PLATFORMS.CLAUDE) { 111 | applyButton.className = platform.buttonStyle.classNames; 112 | applyButton.style.cssText = platform.buttonStyle.style; 113 | buttonContainer.prepend(applyButton); 114 | } 115 | applyButton.innerHTML = platform.buttonStyle.icon; 116 | 117 | setupButtonClickHandler(applyButton, codeBlock); 118 | } 119 | 120 | codeBlock.dataset.buttonAdded = 'true'; 121 | console.log("adding button for codeblock"); 122 | } 123 | 124 | // Helper function for button click handler 125 | function setupButtonClickHandler(button, codeBlock) { 126 | const originalButtonContent = button.innerHTML; 127 | let timeoutId = null; 128 | 129 | const resetButton = () => { 130 | if (timeoutId) { 131 | clearTimeout(timeoutId); 132 | timeoutId = null; 133 | } 134 | button.innerHTML = originalButtonContent; 135 | button.disabled = false; 136 | }; 137 | 138 | button.addEventListener('click', async (e) => { 139 | e.preventDefault(); 140 | e.stopPropagation(); 141 | 142 | // Check WebSocket connection 143 | const isConnected = await isWebSocketConnected(); 144 | 145 | if (!isConnected) { 146 | alert("Cannot connect with VS Code, please ensure EasyCode extension is installed"); 147 | return true; 148 | } 149 | 150 | const platform = getCurrentPlatform(); 151 | const selectors = platform.selectors; 152 | 153 | let code; 154 | if (window.location.hostname.includes('claude.ai')) { 155 | //const containerDiv = button.closest(selectors.codeActionButtonContainer); 156 | //codeblock = containerDiv.querySelector(selectors.codeBlock); 157 | code = codeBlock.textContent 158 | } else { 159 | code = codeBlock.matches(selectors.codeBlock) && codeBlock.querySelector('code') 160 | ? codeBlock.querySelector('code').textContent 161 | : codeBlock.textContent; 162 | } 163 | 164 | console.log('Code block content:', code); 165 | 166 | try { 167 | const similarityScores = predictApplyDestination(code); 168 | const applyDestination = similarityScores.reduce((best, current) => 169 | current.score > best.score ? current : best 170 | ); 171 | 172 | const scoresText = similarityScores 173 | .sort((a, b) => b.score - a.score) 174 | .map(entry => `${entry.fileName}: ${(entry.score * 100).toFixed(1)}%`) 175 | .join('\n'); 176 | 177 | const confirmMessage = `Do you want to apply changes to:\n${applyDestination.fileName}\n\nAll matches:\n${scoresText}`; 178 | 179 | if (confirm(confirmMessage)) { 180 | // Show spinner and disable button 181 | button.innerHTML = SPINNER_SVG; 182 | button.disabled = true; 183 | 184 | // Set timeout to reset button after 2 minutes 185 | timeoutId = setTimeout(() => { 186 | resetButton(); 187 | }, TIMEOUT_DURATION); 188 | 189 | chrome.runtime.sendMessage({ 190 | type: 'APPLY_DIFF', 191 | fileName: applyDestination.fileName, 192 | code: code 193 | }, (response) => { 194 | if (response.error) { 195 | console.error('Error applying changes:', response.error); 196 | alert('Failed to apply changes: ' + response.error); 197 | resetButton(); 198 | } else { 199 | console.log('Changes applied successfully:', response.output); 200 | alert('Changes applied successfully'); 201 | resetButton(); 202 | } 203 | }); 204 | } 205 | } catch (e) { 206 | alert("Failed to apply change, please ensure VS Code extension is running and the right project is open"); 207 | } 208 | }); 209 | } 210 | 211 | 212 | // Rest of the code remains the same 213 | function addButtonsToCodeBlocks() { 214 | const platform = getCurrentPlatform(); 215 | if (!platform) return; 216 | 217 | const codeBlocks = document.querySelectorAll(platform.selectors.codeBlock); 218 | codeBlocks.forEach((codeBlock) => { 219 | if (!Array.from(codeBlock.classList).some(className => className.includes('preview'))) { 220 | addCodeBlockButton(codeBlock); 221 | } 222 | }); 223 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | import mixpanel from "./mixpanel.js" 2 | 3 | mixpanel.init("885bb3993bb98e37dbb21823f8d1903d"); 4 | 5 | chrome.runtime.onInstalled.addListener((details) => { 6 | // tracking extension install 7 | console.log("extension installed"); 8 | mixpanel.track('Install'); 9 | }) 10 | 11 | let websocketPort; // default port 12 | 13 | // Initialize port when extension loads 14 | initializePort(); 15 | 16 | function initializePort() { 17 | chrome.storage.local.get({ websocketPort: 49201 }, (items) => { 18 | websocketPort = items.websocketPort; 19 | connectWebSocket(); 20 | }); 21 | } 22 | 23 | const MAX_RETRIES = 3; 24 | const RETRY_DELAY = 1000; // 1 second between retries 25 | 26 | // Listen for changes to the port setting 27 | chrome.storage.onChanged.addListener((changes, namespace) => { 28 | if (namespace === 'local' && changes.websocketPort) { 29 | websocketPort = changes.websocketPort.newValue; 30 | // Reconnect with new port if socket exists 31 | if (socket) { 32 | socket.close(); 33 | connectWebSocket(); 34 | } 35 | } 36 | }); 37 | 38 | function safeStorageAccess(operation) { 39 | // Check if chrome.storage is available 40 | if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 41 | return operation(); 42 | } else { 43 | console.warn('Chrome storage API not available'); 44 | // Optionally retry after a short delay 45 | setTimeout(() => { 46 | if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) { 47 | operation(); 48 | } 49 | }, 1000); 50 | } 51 | } 52 | 53 | // Helper function to safely send WebSocket messages 54 | function safeSendWebSocketMessage(message) { 55 | if (socket && socket.readyState === WebSocket.OPEN) { 56 | socket.send(JSON.stringify(message)); 57 | return true; 58 | } 59 | console.warn('WebSocket not ready, message not sent:', message); 60 | return false; 61 | } 62 | 63 | chrome.runtime.onInstalled.addListener(() => { 64 | console.log('ChatGPT Mention Extension installed.'); 65 | }); 66 | 67 | let socket; 68 | let reconnectAttempts = 0; 69 | const MAX_RECONNECT_ATTEMPTS = 3; 70 | const RECONNECT_DELAY = 3000; // 3 seconds 71 | 72 | function connectWebSocket(retry = false) { 73 | try{ 74 | socket = new WebSocket(`ws://localhost:${websocketPort}`); 75 | } catch (e) { 76 | console.warn("Failed to establish websocket connect"); 77 | } 78 | 79 | socket.onopen = () => { 80 | console.log('Connected to VS Code extension'); 81 | reconnectAttempts = 0; // Reset attempts on successful connection 82 | 83 | // Add a small delay to ensure the connection is ready 84 | setTimeout(() => { 85 | safeSendWebSocketMessage({ 86 | type: 'REQUEST_FILES' 87 | }); 88 | }, 100); 89 | }; 90 | 91 | socket.onmessage = (event) => { 92 | const data = JSON.parse(event.data); 93 | if (data.type === 'FILE_LIST') { 94 | console.log(`received file list`); 95 | safeStorageAccess(() => { 96 | chrome.storage.local.set({ 97 | filePaths: data.files 98 | }); 99 | }); 100 | } else if (data.type === 'FILE_CONTENTS') { 101 | console.log("Looking for file content callback for:", data.filePath); 102 | if (fileContentCallbacks[data.filePath]) { 103 | // Ensure content is properly decoded 104 | const content = typeof data.content === 'string' 105 | ? data.content 106 | : new TextDecoder('utf-8').decode(data.content); 107 | 108 | fileContentCallbacks[data.filePath]({ 109 | content: content 110 | }); 111 | delete fileContentCallbacks[data.filePath]; 112 | } else { 113 | console.warn("No file content callback found for:", data.filePath); 114 | } 115 | } else if (data.type === 'DIFF_CLIPBOARD_RESULT') { // Assuming this is the type for diff responses 116 | console.log("Looking for diff callback for:", data.fileName); 117 | if (diffCallbacks[data.fileName]) { 118 | diffCallbacks[data.fileName](data); 119 | delete diffCallbacks[data.fileName]; 120 | } else { 121 | console.warn("No diff callback found for:", data.fileName); 122 | } 123 | } 124 | }; 125 | 126 | socket.onerror = (error) => { 127 | console.warn('WebSocket error:', error); 128 | }; 129 | 130 | socket.onclose = () => { 131 | if (!retry) { 132 | console.log('WebSocket closed'); 133 | return; 134 | } else { 135 | // Attempt to reconnect 136 | if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { 137 | reconnectAttempts++; 138 | console.log(`Attempting to reconnect (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`); 139 | setTimeout(connectWebSocket, RECONNECT_DELAY); 140 | } else { 141 | console.warn('Max reconnection attempts reached'); 142 | } 143 | } 144 | }; 145 | } 146 | 147 | // Add a function to check connection status 148 | async function isSocketConnected() { 149 | let isConnected = socket && socket.readyState === WebSocket.OPEN; 150 | if (!isConnected) { 151 | console.log('Socket not connected. Attempting to reconnect...'); 152 | reconnectAttempts = 0; 153 | 154 | // Create a promise that resolves when connection is established or rejects after timeout 155 | const connectionPromise = new Promise((resolve, reject) => { 156 | const originalOnOpen = socket?.onopen; 157 | const originalOnError = socket?.onerror; 158 | 159 | // Set a timeout to reject the promise if connection takes too long 160 | const timeoutId = setTimeout(() => { 161 | reject(new Error('Connection timeout')); 162 | }, 1000); // 5 second timeout 163 | 164 | connectWebSocket(); 165 | 166 | socket.onopen = (event) => { 167 | clearTimeout(timeoutId); 168 | if (originalOnOpen) originalOnOpen(event); 169 | else { 170 | setTimeout(() => { 171 | safeSendWebSocketMessage({ 172 | type: 'REQUEST_FILES' 173 | }); 174 | }, 100); 175 | } 176 | resolve(); 177 | }; 178 | 179 | socket.onerror = (error) => { 180 | clearTimeout(timeoutId); 181 | if (originalOnError) originalOnError(error); 182 | reject(error); 183 | }; 184 | }); 185 | 186 | try { 187 | await connectionPromise; 188 | isConnected = socket && socket.readyState === WebSocket.OPEN; 189 | } catch (error) { 190 | console.warn('Failed to establish connection:', error); 191 | isConnected = false; 192 | } 193 | } 194 | return isConnected; 195 | } 196 | 197 | // Separate callback queues 198 | let fileContentCallbacks = {}; 199 | let diffCallbacks = {}; 200 | 201 | // Add a helper function to handle retries 202 | async function retryOperation(operation, maxRetries = MAX_RETRIES) { 203 | for (let attempt = 0; attempt < maxRetries; attempt++) { 204 | const isConnected = await isSocketConnected(); 205 | if (isConnected) { 206 | return await operation(); 207 | } 208 | 209 | console.log(`WebSocket not connected, attempt ${attempt + 1}/${maxRetries}`); 210 | 211 | // Try to reconnect 212 | connectWebSocket(); 213 | 214 | // Wait for connection 215 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); 216 | } 217 | 218 | throw new Error('WebSocket connection failed after retries'); 219 | } 220 | 221 | // Modify the message listener to use retries 222 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 223 | if (message.type === 'APPLY_DIFF') { 224 | console.log("Background: received request to apply diff for:", message.fileName); 225 | 226 | retryOperation(async () => { 227 | // Store in diff-specific callback queue 228 | diffCallbacks[message.fileName] = (response) => { 229 | console.log("Diff result received:", response); 230 | sendResponse(response); 231 | }; 232 | 233 | if (!safeSendWebSocketMessage({ 234 | type: 'DIFF_CLIPBOARD', 235 | fileName: message.fileName, 236 | code: `${message.code}\n` 237 | })) { 238 | throw new Error('Failed to send diff request'); 239 | } 240 | }) 241 | .catch(error => { 242 | console.log('Error applying diff:', error); 243 | sendResponse({ 244 | error: 'Failed to apply changes after retries. Please check your connection.' 245 | }); 246 | }); 247 | 248 | return true; 249 | } 250 | else if (message.type === 'GET_FILE_CONTENTS') { 251 | console.log("Background: received request for file:", message.filePath); 252 | 253 | retryOperation(async () => { 254 | // Store in file-specific callback queue 255 | fileContentCallbacks[message.filePath] = (response) => { 256 | console.log("Executing callback for:", message.filePath, response); 257 | sendResponse(response); 258 | }; 259 | 260 | if (!safeSendWebSocketMessage({ 261 | type: 'GET_FILE_CONTENTS', 262 | filePath: message.filePath.trim() 263 | })) { 264 | throw new Error('Failed to send file contents request'); 265 | } 266 | }) 267 | .catch(error => { 268 | console.warn('Error getting file contents:', error); 269 | sendResponse({ 270 | error: 'Failed to get file contents after retries. Please check your connection.' 271 | }); 272 | }); 273 | 274 | return true; 275 | } 276 | else if (message.type === 'CHECK_CONNECTION') { 277 | isSocketConnected().then(connected => { 278 | sendResponse({ connected }); 279 | }); 280 | return true; 281 | } else if (message.type === "REQUEST_FILES") { 282 | // Create a promise that resolves when files are updated 283 | const filesUpdatePromise = new Promise((resolve) => { 284 | // Store the resolve function in a callback that will be called 285 | // when we receive the FILE_LIST response 286 | const messageCallback = (event) => { 287 | const data = JSON.parse(event.data); 288 | if (data.type === 'FILE_LIST') { 289 | socket.removeEventListener('message', messageCallback); 290 | resolve(data.files); 291 | } 292 | }; 293 | 294 | // Add temporary listener for this specific request 295 | socket.addEventListener('message', messageCallback); 296 | 297 | // Send the request 298 | safeSendWebSocketMessage({ 299 | type: 'REQUEST_FILES' 300 | }); 301 | 302 | // Add timeout to prevent hanging 303 | setTimeout(() => { 304 | socket.removeEventListener('message', messageCallback); 305 | resolve([]); // Resolve with empty array if timeout 306 | }, 5000); 307 | }); 308 | 309 | // Wait for files to be updated before sending response 310 | filesUpdatePromise.then((files) => { 311 | sendResponse({ success: true, files }); 312 | }); 313 | 314 | return true; // Keep message channel open for async response 315 | } 316 | }); 317 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 318 | if (request.action === 'openOptions') { 319 | chrome.runtime.openOptionsPage(); 320 | } 321 | }); 322 | 323 | chrome.tabs.onActivated.addListener(async (activeInfo) => { 324 | const tab = await chrome.tabs.get(activeInfo.tabId); 325 | await handleTabUrl(tab.url); 326 | }); 327 | 328 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 329 | if (changeInfo.url) { 330 | await handleTabUrl(changeInfo.url); 331 | } 332 | }); 333 | 334 | async function handleTabUrl(url) { 335 | const validUrls = [ 336 | 'https://chat.openai.com', 337 | 'https://chatgpt.com', 338 | 'https://claude.ai' 339 | ]; 340 | 341 | const shouldConnect = validUrls.some(validUrl => url?.startsWith(validUrl)); 342 | 343 | if (shouldConnect) { 344 | const isConnected = await isSocketConnected(); 345 | if (!isConnected) { 346 | chrome.storage.local.get({ 347 | websocketPort 348 | }, (items) => { 349 | websocketPort = items.websocketPort; 350 | connectWebSocket(); 351 | }); 352 | } 353 | } else { 354 | // Disconnect if we're on a non-matching page 355 | if (socket) { 356 | socket.close(); 357 | socket = null; 358 | } 359 | } 360 | } 361 | 362 | async function checkExistingTabs() { 363 | const tabs = await chrome.tabs.query({active: true}); 364 | if (tabs[0]) { 365 | handleTabUrl(tabs[0].url); 366 | } 367 | } 368 | 369 | chrome.runtime.onInstalled.addListener(() => { 370 | checkExistingTabs(); 371 | }); -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // content.js 2 | 3 | async function initializeMentionExtension(inputField) { 4 | inputField.classList.add('mention-extension-enabled'); 5 | 6 | // Add single keyboard event listeners that delegate to menu handler 7 | inputField.addEventListener('keydown', (event) => { 8 | handleMenuKeyboardEvents(event, inputField); 9 | }, { capture: true, passive: false }); 10 | 11 | inputField.addEventListener('keyup', (event) => { 12 | handleMenuKeyboardEvents(event, inputField); 13 | }, true); 14 | 15 | // Close context menu on click outside 16 | document.addEventListener('click', (event) => { 17 | const menu = document.getElementById('mention-context-menu'); 18 | const isAddContextButton = event.target.classList.contains('add-context-btn'); 19 | 20 | if (menu && !menu.contains(event.target) && !isAddContextButton) { 21 | removeContextMenu(); 22 | } 23 | }); 24 | } 25 | 26 | let inputFieldParentContainer; 27 | 28 | // Wrap the observer initialization in a function 29 | function initializeObservers() { 30 | const observer = new MutationObserver(() => { 31 | const selectors = getSelectors(); 32 | if (!selectors) return; 33 | 34 | // Add button to input container 35 | const inputFieldContainer = getInputFieldContainer(); 36 | 37 | if (inputFieldContainer) { 38 | const chipsContainer = createChipsContainer(inputFieldContainer); 39 | addContextButton(chipsContainer); 40 | addSettingsButton(chipsContainer); 41 | } else { 42 | console.log("no found"); 43 | } 44 | 45 | const inputField = document.querySelector(selectors.inputField); 46 | if (inputField && !inputField.classList.contains('mention-extension-enabled')) { 47 | initializeMentionExtension(inputField); 48 | } 49 | }); 50 | 51 | // Code block observer 52 | const codeBlockObserver = new MutationObserver((mutations) => { 53 | const platform = getCurrentPlatform(); 54 | if (!platform) return; 55 | 56 | const selectors = platform.selectors; 57 | const codeBlockSelector = selectors.codeBlock; 58 | 59 | mutations.forEach(mutation => { 60 | if (mutation.type === 'childList') { 61 | mutation.addedNodes.forEach(node => { 62 | if (node.nodeType === Node.ELEMENT_NODE) { 63 | // Check for code blocks using platform-specific selector 64 | if (node.matches(codeBlockSelector)) { 65 | addCodeBlockButton( 66 | node, 67 | platform === PLATFORMS.CHATGPT ? 'chatgpt' : 'claude' 68 | ); 69 | } 70 | 71 | // Also check children using platform-specific selector 72 | const codeBlocks = node.querySelectorAll(codeBlockSelector); 73 | codeBlocks.forEach(codeBlock => { 74 | addCodeBlockButton( 75 | codeBlock, 76 | platform === PLATFORMS.CHATGPT ? 'chatgpt' : 'claude' 77 | ); 78 | }); 79 | } 80 | }); 81 | } 82 | }); 83 | }); 84 | 85 | // Function to start observers 86 | function startObservers() { 87 | if (document.body) { 88 | observer.observe(document.body, { 89 | childList: true, 90 | subtree: true 91 | }); 92 | 93 | codeBlockObserver.observe(document.body, { 94 | childList: true, 95 | subtree: true 96 | }); 97 | } 98 | } 99 | 100 | // Check document readiness and initialize accordingly 101 | if (document.readyState === 'loading') { 102 | document.addEventListener('DOMContentLoaded', startObservers); 103 | } else { 104 | startObservers(); 105 | } 106 | 107 | // Backup timeout in case DOMContentLoaded already fired 108 | setTimeout(startObservers, 1000); 109 | } 110 | 111 | // Initialize everything 112 | initializeObservers(); 113 | 114 | // Initialize code block buttons with proper timing 115 | function initializeCodeBlockButtons() { 116 | //addButtonsToCodeBlocks(); 117 | setTimeout(addButtonsToCodeBlocks, 1000); 118 | //setInterval(addButtonsToCodeBlocks, 2000); 119 | } 120 | 121 | // Start the code block button initialization 122 | if (document.readyState === 'loading') { 123 | console.log("page still loading, wait for DOM load"); 124 | document.addEventListener('DOMContentLoaded', initializeCodeBlockButtons); 125 | } else { 126 | console.log("page loaded, attempting to add buttons"); 127 | initializeCodeBlockButtons(); 128 | } 129 | 130 | // Insert mention content 131 | async function insertMentionContent(inputField, suggestion) { 132 | const container = createChipsContainer(inputField); 133 | 134 | // Check if chip already exists for this file 135 | const existingChips = container.getElementsByClassName('file-chip'); 136 | let hasChipAlready = false; 137 | for (const chip of existingChips) { 138 | if (chip.getAttribute('data-file') === suggestion.label) { 139 | hasChipAlready = true; 140 | console.log("already has chip"); 141 | // Chip already exists, don't add duplicate 142 | } else { 143 | console.log("NOT has chip"); 144 | } 145 | } 146 | 147 | if (hasChipAlready) { 148 | 149 | } else { 150 | const chip = createFileChip(suggestion); 151 | container.appendChild(chip); 152 | } 153 | 154 | if (!(suggestion.label in fileContentCache) && suggestion.type !== 'folder') { 155 | getFileContents(suggestion.label) 156 | .then(content => { 157 | fileContentCache[suggestion.label] = content; 158 | }) 159 | .catch(error => { 160 | console.error('Error caching file content:', error); 161 | }); 162 | } 163 | 164 | // Clean up '>' character and add file name 165 | const currentText = inputField.value || inputField.innerText; 166 | if (currentText.endsWith('>')) { 167 | const newText = currentText.slice(0, -1) + `file: ${suggestion.label} `; 168 | if (inputField.value !== undefined) { 169 | inputField.value = newText; 170 | } else { 171 | inputField.innerText = newText; 172 | } 173 | } 174 | 175 | // Set cursor position 176 | const range = document.createRange(); 177 | const selection = window.getSelection(); 178 | range.selectNodeContents(inputField); 179 | range.collapse(false); 180 | selection.removeAllRanges(); 181 | selection.addRange(range); 182 | 183 | removeContextMenu(); 184 | } 185 | 186 | // Get input field 187 | function getInputField() { 188 | const selectors = getSelectors(); 189 | return document.querySelector(selectors.inputField); 190 | } 191 | 192 | function addContextButton(inputFieldContainer) { 193 | // Check if button already exists 194 | if (inputFieldContainer.querySelector('.add-context-btn')) { 195 | return; 196 | } 197 | 198 | // Create button 199 | const button = document.createElement('button'); 200 | button.className = 'add-context-btn'; 201 | button.innerHTML = '+ Add Context'; 202 | button.style.cssText = ` 203 | top: 10px; 204 | left: 10px; 205 | padding: 6px 12px; 206 | background-color: #333; 207 | color: #fff; 208 | border: 1px solid #555; 209 | border-radius: 15px; 210 | cursor: pointer; 211 | font-size: 12px; 212 | z-index: 1000; 213 | `; 214 | 215 | // Add hover effect 216 | button.addEventListener('mouseover', () => { 217 | button.style.backgroundColor = '#444'; 218 | }); 219 | button.addEventListener('mouseout', () => { 220 | button.style.backgroundColor = '#333'; 221 | }); 222 | 223 | // Add click handler 224 | button.addEventListener('click', () => { 225 | const inputField = getInputField(); 226 | if (inputField) { 227 | // Simulate typing '>' by creating and showing the file menu 228 | 229 | const selection = window.getSelection(); 230 | const range = selection.getRangeAt(0); 231 | showContextMenu(getInputFieldContainer(), range, ""); 232 | } 233 | }); 234 | 235 | // Insert as first child 236 | inputFieldContainer.prepend(button); 237 | } 238 | 239 | function addSettingsButton(inputFieldContainer) { 240 | // Check if link already exists 241 | if (inputFieldContainer.querySelector('.settings-link')) { 242 | return; 243 | } 244 | 245 | // Create container for right alignment 246 | const linkContainer = document.createElement('div'); 247 | linkContainer.style.cssText = ` 248 | display: flex; 249 | justify-content: flex-end; 250 | position: absolute; 251 | right: 10px; 252 | z-index: 1000; 253 | `; 254 | 255 | // Create settings link 256 | const link = document.createElement('span'); 257 | link.className = 'settings-link'; 258 | link.innerHTML = 'EasyCode Settings'; 259 | link.style.cssText = ` 260 | color: #888; 261 | font-size: 11px; 262 | cursor: pointer; 263 | padding: 4px 8px; 264 | opacity: 1; 265 | transition: opacity 0.2s, color 0.2s; 266 | font-family: Arial, sans-serif; 267 | `; 268 | 269 | // Add hover effect 270 | link.addEventListener('mouseover', () => { 271 | link.style.opacity = '1'; 272 | link.style.color = '#aaa'; 273 | }); 274 | link.addEventListener('mouseout', () => { 275 | link.style.opacity = '1'; 276 | link.style.color = '#888'; 277 | }); 278 | 279 | // Add click handler to open options page 280 | link.addEventListener('click', () => { 281 | chrome.runtime.sendMessage({ action: 'openOptions' }); 282 | }); 283 | 284 | // Add link to container and container to inputFieldContainer 285 | linkContainer.appendChild(link); 286 | inputFieldContainer.appendChild(linkContainer); 287 | 288 | // Platform-specific adjustments 289 | const platform = getCurrentPlatform(); 290 | if (platform === PLATFORMS.CHATGPT) { 291 | linkContainer.style.top = '8px'; 292 | linkContainer.style.right = '8px'; 293 | } else if (platform === PLATFORMS.CLAUDE) { 294 | linkContainer.style.top = '10px'; 295 | linkContainer.style.right = '10px'; 296 | } 297 | } -------------------------------------------------------------------------------- /contextMenu.js: -------------------------------------------------------------------------------- 1 | let currentMenuIndex = 0; // Track the currently highlighted menu item 2 | 3 | function shouldShowContextMenu(text, index) { 4 | // Extract text before and after the '>' character 5 | const textBeforeArrow = text.slice(0, index); 6 | const textAfterArrow = text.slice(index + 1); 7 | 8 | // Check if there's no non-whitespace character after '>' 9 | const noTrailingString = !/\S/.test(textAfterArrow); 10 | 11 | // Check if the '>' is at the start or immediately preceded by a whitespace 12 | const hasWhiteSpaceImmediatelyBefore = 13 | index === 0 || /\s/.test(text[index - 1]); 14 | 15 | // Decide whether to show the context menu 16 | return noTrailingString && hasWhiteSpaceImmediatelyBefore; 17 | } 18 | 19 | // Add this function to handle all menu-related keyboard events 20 | function handleMenuKeyboardEvents(event, inputField) { 21 | const menu = document.getElementById('mention-context-menu'); 22 | const inputFieldContainer = getInputFieldContainer(); 23 | 24 | // Handle keydown events when menu is open 25 | if (event.type === 'keydown' && menu) { 26 | if (event.key === 'Enter') { 27 | console.log("captured enter inside mention menu"); 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | event.stopImmediatePropagation(); 31 | 32 | const menuItems = document.querySelectorAll('.mention-menu-item'); 33 | if (menuItems.length > 0 && currentMenuIndex >= 0) { 34 | const selectedItem = menuItems[currentMenuIndex]; 35 | const suggestionLabel = selectedItem.innerText; 36 | 37 | getSuggestions('').then(suggestions => { 38 | const suggestion = suggestions.find(s => s.label === suggestionLabel); 39 | if (selectedItem && suggestion) { 40 | insertMentionContent(inputField, suggestion); 41 | removeContextMenu(); 42 | } 43 | }); 44 | } 45 | return false; 46 | } 47 | } 48 | 49 | // Handle keyup events 50 | if (event.type === 'keyup') { 51 | // Check if menu is open first for navigation 52 | if (menu && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { 53 | console.log(`Navigating menu with key: ${event.key}`); 54 | event.preventDefault(); 55 | event.stopPropagation(); 56 | navigateMenu(event.key); 57 | return; 58 | } 59 | 60 | if (event.key === ">") { 61 | // Get the current selection 62 | const selection = window.getSelection(); 63 | const range = selection.getRangeAt(0); 64 | const currentNode = range.startContainer; 65 | 66 | // Check for context menu trigger 67 | const textContent = currentNode.textContent; 68 | const cursorPosition = range.startOffset; 69 | const textBeforeCursor = textContent.slice(0, cursorPosition); 70 | 71 | const lastIndex = textBeforeCursor.lastIndexOf('>'); 72 | const shouldShowFileSuggestions = shouldShowContextMenu(textBeforeCursor, lastIndex); 73 | 74 | if (lastIndex !== -1 && shouldShowFileSuggestions) { 75 | const query = textBeforeCursor.slice(lastIndex + 1); 76 | showContextMenu(getInputFieldContainer(), range, query.trim()); 77 | } else { 78 | removeContextMenu(); 79 | } 80 | } 81 | } 82 | } 83 | 84 | function navigateMenu(direction, menuItems) { 85 | if (menuItems.length === 0) return; 86 | 87 | // Clear previous highlight 88 | if (currentMenuIndex >= 0 && menuItems[currentMenuIndex]) { 89 | menuItems[currentMenuIndex].classList.remove('highlighted'); 90 | } 91 | 92 | // Update the index based on the direction 93 | if (direction === 'ArrowDown') { 94 | currentMenuIndex = (currentMenuIndex + 1) % menuItems.length; 95 | } else if (direction === 'ArrowUp') { 96 | currentMenuIndex = (currentMenuIndex - 1 + menuItems.length) % menuItems.length; 97 | } 98 | 99 | // Highlight the current menu item 100 | const currentItem = menuItems[currentMenuIndex]; 101 | currentItem.classList.add('highlighted'); 102 | 103 | // Scroll the highlighted item into view 104 | currentItem.scrollIntoView({ block: 'nearest' }); 105 | } 106 | 107 | function handleMenuInputKeyDown(event, menuInput, menu) { 108 | const menuItems = menu.querySelectorAll('.mention-menu-item'); 109 | 110 | if (event.key === 'ArrowDown') { 111 | event.preventDefault(); 112 | navigateMenu('ArrowDown', menuItems); 113 | } else if (event.key === 'ArrowUp') { 114 | event.preventDefault(); 115 | navigateMenu('ArrowUp', menuItems); 116 | } else if (event.key === 'Enter') { 117 | console.log("Captured Enter inside FileMenu") 118 | event.preventDefault(); 119 | if (currentMenuIndex >= 0 && menuItems[currentMenuIndex]) { 120 | const selectedItem = menuItems[currentMenuIndex]; 121 | const suggestionLabel = selectedItem.innerText; 122 | getSuggestions('').then((suggestions) => { 123 | const suggestion = suggestions.find(s => s.label === suggestionLabel); 124 | if (suggestion) { 125 | insertMentionContent(document.querySelector(getSelectors().inputField), suggestion); 126 | //removeContextMenu(); 127 | } 128 | }); 129 | } 130 | } else if (event.key === 'Escape') { 131 | console.log("Captured Escape inside FileMenu") 132 | removeContextMenu(); 133 | } 134 | } 135 | 136 | // Add this helper function to check WebSocket connection 137 | async function isWebSocketConnected() { 138 | // Send a message to background script to check connection status 139 | return new Promise(resolve => { 140 | chrome.runtime.sendMessage({ type: 'CHECK_CONNECTION' }, response => { 141 | resolve(response?.connected || false); 142 | }); 143 | }); 144 | } 145 | 146 | async function showContextMenu(inputField, range, query) { 147 | removeContextMenu(); 148 | 149 | const menu = document.createElement('div'); 150 | menu.id = 'mention-context-menu'; 151 | document.body.appendChild(menu); 152 | 153 | // Position the menu 154 | const inputRect = inputField.getBoundingClientRect(); 155 | const menuTop = inputRect.top; 156 | const menuLeft = inputRect.left; 157 | 158 | menu.style.setProperty('--menu-top', `${menuTop}px`); 159 | menu.style.left = `${menuLeft}px`; 160 | menu.style.width = `${inputRect.width}px`; 161 | menu.style.maxHeight = '200px'; 162 | menu.style.overflowY = 'auto'; 163 | menu.style.border = '1px solid #ccc'; 164 | menu.style.borderRadius = '15px'; 165 | menu.style.zIndex = '1000'; 166 | 167 | const menuInput = document.createElement('input'); 168 | menuInput.type = 'text'; 169 | menuInput.id = 'menu-input'; 170 | menuInput.value = query; 171 | menuInput.placeholder = 'Search files...'; 172 | menuInput.className = 'menu-input'; 173 | 174 | // Create a suggestions container 175 | const suggestionsContainer = document.createElement('div'); 176 | suggestionsContainer.id = 'suggestions-container'; 177 | 178 | // Append both elements to the menu 179 | menu.appendChild(menuInput); 180 | menu.appendChild(suggestionsContainer); 181 | 182 | // Initial suggestions 183 | await updateSuggestions(suggestionsContainer, query); 184 | 185 | // Add event listeners 186 | menuInput.addEventListener('input', async (event) => { 187 | currentMenuIndex = 0; 188 | await updateSuggestions(suggestionsContainer, event.target.value); 189 | }); 190 | 191 | menuInput.addEventListener('keydown', (event) => { 192 | handleMenuInputKeyDown(event, menuInput, menu); 193 | }); 194 | 195 | // Focus on the menu input 196 | menuInput.focus(); 197 | 198 | isWebSocketConnected().then(connected => { 199 | if(!connected) { 200 | // remove 201 | menu.removeChild(menuInput); 202 | menu.removeChild(suggestionsContainer); 203 | 204 | // Create a message container for the disconnected state 205 | const disconnectedMessage = document.createElement('div'); 206 | disconnectedMessage.style.cssText = ` 207 | height: 100%; 208 | padding: 16px; 209 | background: #f8f9fa; 210 | color: #333; 211 | font-size: 13px; 212 | line-height: 1.5; 213 | `; 214 | 215 | // Create message text 216 | const messageText = document.createElement('p'); 217 | messageText.style.margin = '0 0 12px 0'; 218 | messageText.innerHTML = `Error: Failed to retrieve files. 219 |

220 | To add files, please install the 221 | EasyCode 222 | companion extension in VS Code. 223 |
224 | The VS Code extension establishes a local connection to serve file content and apply changes. 225 | `; 226 | // Create settings link 227 | const settingsLink = document.createElement('a'); 228 | settingsLink.textContent = 'Learn more in settings'; 229 | settingsLink.href = '#'; 230 | settingsLink.style.cssText = ` 231 | color: #2563eb; 232 | text-decoration: underline; 233 | cursor: pointer; 234 | `; 235 | settingsLink.addEventListener('click', (e) => { 236 | e.preventDefault(); 237 | chrome.runtime.sendMessage({ action: 'openOptions' }); 238 | removeContextMenu(); 239 | }); 240 | 241 | // Append elements 242 | disconnectedMessage.appendChild(messageText); 243 | disconnectedMessage.appendChild(settingsLink); 244 | menu.appendChild(disconnectedMessage); 245 | } 246 | }); 247 | } 248 | 249 | async function updateSuggestions(menu, query) { 250 | try { 251 | menu.innerHTML = ''; 252 | const suggestions = await getSuggestions(query); 253 | 254 | if (suggestions.length === 0) { 255 | const item = document.createElement('div'); 256 | item.className = 'mention-menu-item no-results'; 257 | item.innerText = 'No results found'; 258 | menu.appendChild(item); 259 | return; 260 | } 261 | 262 | suggestions.forEach((suggestion, index) => { 263 | const item = document.createElement('div'); 264 | item.className = 'mention-menu-item'; 265 | item.innerText = suggestion.label; 266 | 267 | item.addEventListener('mousedown', (e) => { 268 | e.preventDefault(); 269 | }); 270 | 271 | item.addEventListener('click', () => { 272 | insertMentionContent(document.querySelector(getSelectors().inputField), suggestion); 273 | //removeContextMenu(); 274 | }); 275 | 276 | menu.appendChild(item); 277 | 278 | if (index === currentMenuIndex) { 279 | item.classList.add('highlighted'); 280 | } 281 | }); 282 | } catch (error) { 283 | console.error('Error getting suggestions:', error); 284 | const errorItem = document.createElement('div'); 285 | errorItem.className = 'mention-menu-item error'; 286 | errorItem.innerText = 'Error loading suggestions'; 287 | menu.appendChild(errorItem); 288 | } 289 | } 290 | 291 | function removeContextMenu() { 292 | const existingMenu = document.getElementById('mention-context-menu'); 293 | if (existingMenu) { 294 | existingMenu.parentNode.removeChild(existingMenu); 295 | // Return focus to the original input field 296 | const inputField = getInputField(); 297 | inputField.focus(); 298 | } 299 | } -------------------------------------------------------------------------------- /easycode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EasyCode-AI/Codaware/795229b5245ebcc06b643d376334d6718781ff7b/easycode.png -------------------------------------------------------------------------------- /fileChips.js: -------------------------------------------------------------------------------- 1 | // Add functions to create and manage chips 2 | function createChipsContainer(inputFieldParentContainer) { 3 | let container = document.getElementById('file-chips-container'); 4 | if (!container) { 5 | container = document.createElement('div'); 6 | container.id = 'file-chips-container'; 7 | container.style.cssText = ` 8 | padding: 4px 0px; 9 | display: flex; 10 | flex-wrap: wrap; 11 | gap: 8px; 12 | `; 13 | inputFieldParentContainer.prepend(container); 14 | } 15 | return container; 16 | } 17 | 18 | // Clear cache when chip is removed 19 | function createFileChip(suggestion) { 20 | const chip = document.createElement('div'); 21 | chip.className = 'file-chip'; 22 | chip.setAttribute('data-file', suggestion.label); // Add data attribute to identify the file 23 | chip.style.cssText = ` 24 | background: #2A2B32; 25 | border: 1px solid #565869; 26 | border-radius: 10px; 27 | padding: 4px 8px; 28 | font-size: 12px; 29 | display: flex; 30 | align-items: center; 31 | gap: 4px; 32 | color: white; 33 | `; 34 | 35 | const icon = suggestion.type === 'folder' ? '📁' : '📄'; 36 | chip.textContent = `${icon} ${suggestion.label}`; 37 | 38 | const removeBtn = document.createElement('span'); 39 | removeBtn.textContent = '×'; 40 | removeBtn.style.marginLeft = '4px'; 41 | removeBtn.style.cursor = 'pointer'; 42 | removeBtn.onclick = () => { 43 | chip.remove(); 44 | }; 45 | chip.appendChild(removeBtn); 46 | 47 | return chip; 48 | } 49 | 50 | // Modify processFileChips to use cached content 51 | async function processFileChips(fileChips) { 52 | return Promise.all( 53 | Array.from(fileChips).map(async chip => { 54 | const label = chip.textContent.split('×')[0].trim().slice(2).trim(); 55 | 56 | try { 57 | let content = fileContentCache[label]; 58 | if (!content) { 59 | content = await getFileContents(label); 60 | // Ensure content is properly formatted before caching 61 | content = content.replace(/\r\n/g, '\n').trim(); 62 | fileContentCache[label] = content; 63 | console.log("Initial file retrieval"); 64 | } else { 65 | console.log("Cached file retrieval"); 66 | } 67 | // console.log("file content:\n", content); 68 | // Add explicit formatting for the file content 69 | return `filepath:${label}\n\`\`\`\n${content}\n\`\`\`\n`; 70 | } catch (error) { 71 | console.error('Error getting content for', label, error); 72 | alert(`File: ${label}\n\`\`\`\nError loading file content\n\`\`\`\n`); 73 | } 74 | }) 75 | ); 76 | } 77 | 78 | // Helper function to append file contents to message 79 | async function appendFileContentsToMessage(fileContents) { 80 | const platform = getCurrentPlatform(); 81 | const editor = document.querySelector(platform.selectors.editor); 82 | const currentText = editor.innerText; 83 | 84 | // Format the file contents with proper paragraph tags 85 | const formattedFileContents = fileContents.map(content => { 86 | return content 87 | .split('\n') 88 | .map(line => { 89 | const escapedLine = line 90 | .replace(//g, '>'); 92 | return `

${escapedLine || '
'}

`; 93 | }) 94 | .join(''); 95 | }); 96 | 97 | const newText = `${currentText}\n\nReferenced Files:\n${formattedFileContents.join('\n\n')}`; 98 | 99 | if (platform.inputFieldType === 'contenteditable') { 100 | editor.innerHTML = newText; 101 | } else { 102 | editor.value = newText; 103 | } 104 | 105 | // Dispatch appropriate input event based on platform 106 | const event = platform.inputFieldType === 'contenteditable' 107 | ? new InputEvent('input', { bubbles: true, cancelable: true }) 108 | : new Event('input', { bubbles: true }); 109 | 110 | // needed otherwise the file doesn't get injected in time before the submission 111 | await new Promise(resolve => setTimeout(resolve, 500)); 112 | 113 | editor.dispatchEvent(event); 114 | } 115 | -------------------------------------------------------------------------------- /files.js: -------------------------------------------------------------------------------- 1 | // Add a cache object to store file contents 2 | let fileContentCache = {}; 3 | 4 | // Function to extract file paths from the page content 5 | function extractFilePaths() { 6 | const filePathRegex = /filepath:([^\s]+)/g; 7 | 8 | const textNodes = document.evaluate( 9 | "//text()", 10 | document.body, 11 | null, 12 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 13 | null 14 | ); 15 | 16 | const paths = new Set(); 17 | 18 | for (let i = 0; i < textNodes.snapshotLength; i++) { 19 | const text = textNodes.snapshotItem(i).textContent; 20 | const matches = text.matchAll(filePathRegex); 21 | for (const match of matches) { 22 | paths.add(match[1]); 23 | } 24 | } 25 | 26 | return Array.from(paths); 27 | } 28 | 29 | function clearFileCache() { 30 | fileContentCache = {}; 31 | } 32 | 33 | // Function to populate the cache 34 | async function populateFileCache() { 35 | const paths = extractFilePaths(); 36 | console.log('Found file paths to cache:', paths); 37 | 38 | for (const path of paths) { 39 | try { 40 | if (!fileContentCache[path]) { 41 | console.log('Fetching content for:', path); 42 | const content = await getFileContents(path); 43 | fileContentCache[path] = content; 44 | } 45 | } catch (error) { 46 | console.error(`Failed to cache content for ${path}:`, error); 47 | } 48 | } 49 | } 50 | 51 | // Modified getFileContents with retry logic 52 | async function getFileContents(filePath, retryCount = 3) { 53 | filePath = filePath.trim(); 54 | 55 | for (let attempt = 1; attempt <= retryCount; attempt++) { 56 | try { 57 | return await new Promise((resolve, reject) => { 58 | console.log(`Attempt ${attempt}: Sending request for file:`, filePath); 59 | 60 | const timeout = setTimeout(() => { 61 | reject(new Error(`Timeout requesting file: ${filePath}`)); 62 | }, 10000); 63 | 64 | chrome.runtime.sendMessage( 65 | { type: 'GET_FILE_CONTENTS', filePath }, 66 | response => { 67 | clearTimeout(timeout); 68 | 69 | if (chrome.runtime.lastError) { 70 | reject(chrome.runtime.lastError); 71 | return; 72 | } 73 | 74 | if (!response) { 75 | reject(new Error('No response received')); 76 | return; 77 | } 78 | 79 | if (response.error) { 80 | reject(new Error(response.error)); 81 | } else { 82 | resolve(response.content); 83 | } 84 | } 85 | ); 86 | }); 87 | } catch (error) { 88 | if (attempt === retryCount) { 89 | throw error; 90 | } 91 | // Wait before retrying 92 | await new Promise(resolve => setTimeout(resolve, 1000)); 93 | } 94 | } 95 | } 96 | 97 | function getSuggestions(query, retryCount = 3, delay = 1000) { 98 | return new Promise((resolve, reject) => { 99 | const tryGetSuggestions = (attemptNumber) => { 100 | if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) { 101 | if (attemptNumber < retryCount) { 102 | console.log(`Chrome storage not available, retrying in ${delay}ms (attempt ${attemptNumber + 1}/${retryCount})`); 103 | setTimeout(() => tryGetSuggestions(attemptNumber + 1), delay); 104 | return; 105 | } 106 | reject(new Error('Chrome storage API not available')); 107 | return; 108 | } 109 | 110 | try { 111 | chrome.storage.local.get(['filePaths'], (result) => { 112 | if (chrome.runtime.lastError) { 113 | reject(chrome.runtime.lastError); 114 | return; 115 | } 116 | 117 | const files = result.filePaths || []; 118 | 119 | const suggestions = files.map(file => ({ 120 | label: file, 121 | type: file.endsWith('/') ? 'folder' : 'file' 122 | })); 123 | 124 | /* 125 | // add special entries such as "problems" 126 | const suggestions = [ 127 | { label: 'problems', type: 'special' }, 128 | ...files.map(file => ({ 129 | label: '/' + file, 130 | type: file.endsWith('/') ? 'folder' : 'file' 131 | })) 132 | ]; 133 | */ 134 | 135 | if (!query) { 136 | resolve(suggestions); 137 | return; 138 | } 139 | 140 | const lowerQuery = query.toLowerCase(); 141 | resolve(suggestions.filter(item => 142 | item.label.toLowerCase().includes(lowerQuery) 143 | )); 144 | }); 145 | } catch (error) { 146 | reject(error); 147 | } 148 | }; 149 | 150 | tryGetSuggestions(0); 151 | }); 152 | } 153 | 154 | // Add URL tracking variable 155 | let currentPath = window.location.pathname; 156 | 157 | // Modified initializeCache function 158 | function initializeCache() { 159 | // Function to check if DOM is ready for processing 160 | function isDOMReady() { 161 | return document.readyState === 'complete' && 162 | document.body !== null && 163 | document.documentElement !== null; 164 | } 165 | 166 | // Function to wait for DOM to be ready 167 | function waitForDOM(callback, maxAttempts = 3) { 168 | let attempts = 0; 169 | 170 | function checkDOM() { 171 | attempts++; 172 | if (isDOMReady()) { 173 | console.log('DOM is ready, proceeding with cache population'); 174 | callback(); 175 | } else if (attempts < maxAttempts) { 176 | console.log(`DOM not ready, attempt ${attempts}/${maxAttempts}. Retrying...`); 177 | setTimeout(checkDOM, 500); 178 | } else { 179 | console.warn('Max attempts reached waiting for DOM. Proceeding anyway...'); 180 | callback(); 181 | } 182 | } 183 | 184 | checkDOM(); 185 | } 186 | 187 | // Setup route observer only when DOM is ready 188 | function setupRouteObserver() { 189 | waitForDOM(() => { 190 | const observer = new MutationObserver((mutations) => { 191 | const newPath = window.location.pathname; 192 | if (newPath !== currentPath) { 193 | console.log('SPA route changed, updating cache...', newPath); 194 | currentPath = newPath; 195 | clearFileCache(); 196 | setTimeout(() => { 197 | console.log('inside page change'); 198 | populateFileCache(); 199 | }, 2000); 200 | } 201 | }); 202 | 203 | observer.observe(document.body, { 204 | childList: true, 205 | subtree: true 206 | }); 207 | 208 | console.log('Route observer set up successfully'); 209 | }); 210 | } 211 | 212 | // Handle navigation events 213 | window.addEventListener('popstate', () => { 214 | const newPath = window.location.pathname; 215 | if (newPath !== currentPath) { 216 | console.log('Navigation occurred, updating cache...', newPath); 217 | currentPath = newPath; 218 | clearFileCache(); 219 | setTimeout(() => { 220 | console.log('inside page refresh'); 221 | populateFileCache(); 222 | }, 2000); } 223 | }); 224 | 225 | // Initial cache population 226 | waitForDOM(() => { 227 | console.log('Starting initial cache population'); 228 | setTimeout(() => { 229 | console.log('inside initial cache population'); 230 | populateFileCache(); 231 | }, 2000); 232 | 233 | }); 234 | 235 | // Set up route observer 236 | setupRouteObserver(); 237 | } 238 | 239 | // Call initialization 240 | initializeCache(); 241 | 242 | function predictApplyDestination(code, filesList) { 243 | if (!code) { 244 | return "ERROR: NO CODE PROVIDED"; 245 | } 246 | if (!filesList) { 247 | filesList = fileContentCache; 248 | } 249 | if (Object.keys(filesList).length === 0) { 250 | return "ERROR: NO FILES AVAILABLE FOR MATCHING"; 251 | } 252 | 253 | try { 254 | // Use findBestMatch to predict the destination 255 | const bestMatch = findBestMatch(code, filesList); 256 | console.log('Best matching file:', bestMatch); 257 | return bestMatch; 258 | } catch (error) { 259 | console.low('Error in predictApplyDestination:', error); 260 | throw new Error("ERROR: MATCHING FAILED"); 261 | } 262 | 263 | //return `client/src/shared/utils/toast.js`; 264 | } 265 | -------------------------------------------------------------------------------- /interceptSubmit.js: -------------------------------------------------------------------------------- 1 | // interceptSubmit.js 2 | 3 | (function () { 4 | let isCustomEvent = false; 5 | 6 | // Common submission handling logic 7 | async function handleSubmission(event) { 8 | event.preventDefault(); 9 | event.stopPropagation(); 10 | //event.stopImmediatePropagation(); 11 | console.log("----- intercepted submission") 12 | 13 | const container = document.getElementById('file-chips-container'); 14 | const fileChips = container ? container.querySelectorAll('.file-chip') : []; 15 | 16 | if (fileChips.length > 0) { 17 | try { 18 | const fileContents = await processFileChips(fileChips); 19 | await appendFileContentsToMessage(fileContents); 20 | await cleanupAndProceed(event); 21 | } catch (error) { 22 | console.error('Error processing file contents:', error); 23 | } 24 | } else { 25 | // If no files to process, proceed with original event 26 | proceedWithOriginalEvent(event); 27 | } 28 | } 29 | 30 | // Helper function to cleanup and proceed with submission 31 | function cleanupAndProceed(event) { 32 | const container = document.getElementById('file-chips-container'); 33 | if (container) { 34 | container.remove(); 35 | } 36 | 37 | proceedWithOriginalEvent(event); 38 | } 39 | 40 | // Function to proceed with original event 41 | function proceedWithOriginalEvent(event) { 42 | isCustomEvent = true; 43 | 44 | if (event instanceof KeyboardEvent) { 45 | // Simulate Enter key press 46 | const resumedEvent = new KeyboardEvent('keydown', { 47 | key: 'Enter', 48 | keyCode: 13, 49 | bubbles: true, 50 | cancelable: true 51 | }); 52 | event.target.dispatchEvent(resumedEvent); 53 | } else if (event instanceof MouseEvent) { 54 | // Simulate button click 55 | const button = event.target; 56 | button.removeAttribute('data-mention-intercepted'); 57 | button.dispatchEvent(new MouseEvent('click', { 58 | bubbles: true, 59 | cancelable: true, 60 | view: window 61 | })); 62 | button.setAttribute('data-mention-intercepted', 'true'); 63 | } 64 | 65 | isCustomEvent = false; 66 | console.log("----- resumed submission") 67 | 68 | } 69 | 70 | // Wrap observer initialization in a function 71 | function initializeSendButtonObserver() { 72 | if (!document.body) { 73 | // If body isn't ready, try again soon 74 | setTimeout(initializeSendButtonObserver, 100); 75 | return; 76 | } 77 | 78 | const sendButtonObserver = new MutationObserver((mutations, observer) => { 79 | const selectors = getSelectors(); 80 | const sendButton = document.querySelector(selectors.sendButton); 81 | 82 | if (sendButton && !sendButton.hasAttribute('data-mention-intercepted')) { 83 | sendButton.setAttribute('data-mention-intercepted', 'true'); 84 | 85 | sendButton.addEventListener('click', async (event) => { 86 | if (event.isTrusted && !isCustomEvent) { 87 | await handleSubmission(event); 88 | } 89 | }, true); 90 | } 91 | }); 92 | 93 | // Start observing once body is available 94 | sendButtonObserver.observe(document.body, { 95 | childList: true, 96 | subtree: true 97 | }); 98 | } 99 | 100 | // Initialize the observer with proper timing 101 | if (document.readyState === 'loading') { 102 | document.addEventListener('DOMContentLoaded', initializeSendButtonObserver); 103 | } else { 104 | initializeSendButtonObserver(); 105 | } 106 | 107 | // Add helper to check if element matches platform input field 108 | function isInPlatformInputField(element) { 109 | const selectors = getSelectors(); // Get current platform selectors 110 | if (!selectors) return false; 111 | 112 | const inputField = document.querySelector(selectors.inputField); 113 | return inputField === element; 114 | } 115 | 116 | // Add helper to check if element is part of mention context menu 117 | function isInMentionContextMenu(element) { 118 | const menu = document.getElementById('mention-context-menu'); 119 | return menu && (menu === element || menu.contains(element)); 120 | } 121 | 122 | 123 | // Keep the Enter key interceptor 124 | window.addEventListener( 125 | 'keydown', 126 | async function (event) { 127 | const menu = document.getElementById('mention-context-menu'); 128 | 129 | // Only proceed if it's Enter without any modifier keys and not a custom event 130 | if ((event.key === 'Enter' || event.keyCode === 13) && 131 | !event.shiftKey && 132 | !event.ctrlKey && 133 | !event.altKey && 134 | !event.metaKey && 135 | !isCustomEvent) { 136 | if (menu) { 137 | // If there's an active mention menu, don't intercept 138 | // Let the mention handler in content.js handle it 139 | console.log("do not intercept"); 140 | return; 141 | } 142 | 143 | // Otherwise proceed with submission handling 144 | await handleSubmission(event); 145 | } else if (event.key === 'Escape') { 146 | if (menu) { 147 | console.log("Captured Escape inside FileMenu"); 148 | removeContextMenu(); 149 | } 150 | } 151 | }, 152 | { capture: true } 153 | ); 154 | 155 | })(); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Codaware, by EasyCode", 4 | "version": "1.1", 5 | "description": "Chat with codebase and apply changes directly.", 6 | "icons": { 7 | "128": "easycode.png" 8 | }, 9 | "permissions": ["activeTab", "storage"], 10 | "host_permissions": [ 11 | "https://chat.openai.com/*", 12 | "https://chatgpt.com/*", 13 | "https://claude.ai/*", 14 | "https://api.mixpanel.com/*", 15 | "ws://localhost:*/*" 16 | ], 17 | "options_page": "options.html", 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "https://chat.openai.com/*", 22 | "https://chatgpt.com/*", 23 | "https://claude.ai/*" 24 | ], 25 | "js": [ 26 | "interceptSubmit.js", 27 | "utils.js", 28 | "platform.js", 29 | "files.js", 30 | "contextMenu.js", 31 | "fileChips.js", 32 | "applyChange.js", 33 | "content.js" 34 | ], 35 | "css": ["styles.css"], 36 | "run_at": "document_start" 37 | } 38 | ], 39 | "background": { 40 | "service_worker": "background.js", 41 | "type": "module" 42 | } 43 | } -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Codaware - EasyCode Extension Settings 7 | 111 | 112 | 113 |
114 |

Codaware - EasyCode Settings

115 | 116 |
117 | 118 |
119 |
120 | 121 | 122 |
123 |

Common Problems

124 | 128 |
129 | 130 | 131 |
132 | 133 | 134 | 135 |
136 | 137 | 138 |
139 |

Files

140 |
141 | 142 | 143 |
144 | 145 |
146 |
147 |
148 | 149 | 150 |
151 |

FAQ

152 | 158 |
159 | 160 |
161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | // Save options to chrome.storage 2 | function saveOptions() { 3 | const port = document.getElementById('port').value; 4 | chrome.storage.local.set({ 5 | websocketPort: parseInt(port) || 49201 6 | }, () => { 7 | // Update status to let user know options were saved 8 | const status = document.getElementById('status'); 9 | status.textContent = 'Options saved.'; 10 | setTimeout(() => { 11 | status.textContent = ''; 12 | }, 2000); 13 | }); 14 | } 15 | 16 | // Clear file paths cache 17 | function clearFilesCache() { 18 | const status = document.getElementById('status'); 19 | chrome.storage.local.remove('filePaths', () => { 20 | status.textContent = 'Files cache cleared.'; 21 | 22 | chrome.runtime.sendMessage({ type: 'REQUEST_FILES' }, response => { 23 | if (response && response.success) { 24 | status.textContent = 'Files cache updated.'; 25 | displayCachedFiles(); 26 | } else { 27 | status.textContent = 'Failed to update files cache.'; 28 | } 29 | }); 30 | }); 31 | } 32 | 33 | async function isWebSocketConnected() { 34 | // Send a message to background script to check connection status 35 | return new Promise(resolve => { 36 | chrome.runtime.sendMessage({ type: 'CHECK_CONNECTION' }, response => { 37 | resolve(response?.connected || false); 38 | }); 39 | }); 40 | } 41 | 42 | // Check WebSocket connection status 43 | function checkConnection() { 44 | try { 45 | const status = document.getElementById('status'); 46 | 47 | isWebSocketConnected().then(connected => { 48 | 49 | if (connected) { 50 | status.textContent = 'Connected to VS Code.'; 51 | status.style.color = '#4CAF50'; 52 | } else { 53 | status.textContent = 'Not connected to VS Code. Make sure EasyCode extension is running in VS Code.'; 54 | status.style.color = '#F44336'; 55 | } 56 | 57 | }); 58 | 59 | 60 | } catch (error) { 61 | console.error('Error checking connection:', error); 62 | const status = document.getElementById('status'); 63 | status.textContent = 'Error checking connection.'; 64 | status.style.color = '#F44336'; 65 | } 66 | } 67 | 68 | // Restores select box and checkbox state using the preferences 69 | // stored in chrome.storage. 70 | function restoreOptions() { 71 | chrome.storage.local.get({ 72 | websocketPort: 49201 // default value 73 | }, (items) => { 74 | document.getElementById('port').value = items.websocketPort; 75 | }); 76 | } 77 | 78 | // Add this function to display cached files 79 | function displayCachedFiles() { 80 | const cachedFilesElement = document.getElementById('cachedFiled'); 81 | 82 | chrome.storage.local.get('filePaths', (result) => { 83 | const files = result.filePaths || []; 84 | 85 | if (files.length === 0) { 86 | cachedFilesElement.innerHTML = '

No cached files found. Please make sure EasyCode extension is running in VS Code

'; 87 | return; 88 | } 89 | 90 | const fileList = document.createElement('ul'); 91 | fileList.style.cssText = 'list-style: none; padding: 0; max-height: 300px; overflow-y: auto;'; 92 | 93 | files.forEach(file => { 94 | const li = document.createElement('li'); 95 | li.textContent = file; 96 | li.style.cssText = 'padding: 4px 0; border-bottom: 1px solid #eee;'; 97 | fileList.appendChild(li); 98 | }); 99 | 100 | cachedFilesElement.innerHTML = ''; // Clear previous content 101 | cachedFilesElement.appendChild(fileList); 102 | 103 | // Add file count 104 | const countDiv = document.createElement('div'); 105 | countDiv.textContent = `Total files: ${files.length}`; 106 | countDiv.style.marginTop = '10px'; 107 | cachedFilesElement.appendChild(countDiv); 108 | }); 109 | } 110 | 111 | document.addEventListener('DOMContentLoaded', restoreOptions); 112 | document.getElementById('save').addEventListener('click', saveOptions); 113 | document.getElementById('clearCache').addEventListener('click', clearFilesCache); 114 | document.getElementById('checkConnection').addEventListener('click', checkConnection); 115 | 116 | // Existing options code for port settings... 117 | 118 | const commonIgnoredPatterns = [ 119 | // Update patterns to match paths more accurately 120 | /[\/\\]node_modules[\/\\]/, // This will match node_modules anywhere in the path 121 | /[\/\\]\.git[\/\\]/, 122 | /[\/\\]\.DS_Store$/, 123 | /[\/\\]\.env$/, 124 | /[\/\\]\.vscode[\/\\]/, 125 | /[\/\\]\.idea[\/\\]/, 126 | /[\/\\]dist[\/\\]/, 127 | /[\/\\]build[\/\\]/, 128 | /\.png$/, 129 | /\.jpg$/, 130 | /\.jpeg$/, 131 | /\.ico$/, 132 | /\.log$/, 133 | /\.lock$/, 134 | /package-lock\.json$/, 135 | /yarn\.lock$/ 136 | ]; 137 | 138 | async function updateProgress(fileListElement, message) { 139 | const progressDiv = fileListElement.querySelector('.progress') || (() => { 140 | const div = document.createElement('div'); 141 | div.className = 'progress'; 142 | fileListElement.appendChild(div); 143 | return div; 144 | })(); 145 | progressDiv.textContent = message; 146 | } 147 | 148 | // Function to parse ignore file contents into patterns 149 | async function parseIgnoreFile(fileHandle) { 150 | try { 151 | const file = await fileHandle.getFile(); 152 | const contents = await file.text(); 153 | 154 | return contents 155 | .split('\n') 156 | .map(line => line.trim()) 157 | .filter(line => line && !line.startsWith('#')) 158 | .map(pattern => { 159 | // Convert glob patterns to RegExp 160 | pattern = pattern 161 | .replace(/\./g, '\\.') 162 | .replace(/\*/g, '.*') 163 | .replace(/\?/g, '.'); 164 | return new RegExp(pattern); 165 | }); 166 | } catch (error) { 167 | console.warn('Error reading ignore file:', error); 168 | return []; 169 | } 170 | } 171 | 172 | // Improved shouldIgnorePath function 173 | function shouldIgnorePath(path, ignorePatterns) { 174 | // Normalize path separators 175 | const normalizedPath = path.replace(/\\/g, '/'); 176 | return ignorePatterns.some(pattern => pattern.test(normalizedPath)); 177 | } 178 | 179 | // Modified listFilesRecursively function 180 | async function* listFilesRecursively(dirHandle, path = '', fileListElement, ignorePatterns = null) { 181 | try { 182 | // Load ignore patterns if we're at the root and haven't loaded them yet 183 | if (!ignorePatterns) { 184 | ignorePatterns = [...commonIgnoredPatterns]; 185 | 186 | try { 187 | // Try to read .gitignore 188 | const gitignoreHandle = await dirHandle.getFileHandle('.gitignore'); 189 | const gitignorePatterns = await parseIgnoreFile(gitignoreHandle); 190 | ignorePatterns.push(...gitignorePatterns); 191 | } catch (error) { 192 | // .gitignore doesn't exist, ignore error 193 | } 194 | 195 | try { 196 | // Try to read easycode.ignore 197 | const easycodeIgnoreHandle = await dirHandle.getFileHandle('easycode.ignore'); 198 | const easycodeIgnorePatterns = await parseIgnoreFile(easycodeIgnoreHandle); 199 | ignorePatterns.push(...easycodeIgnorePatterns); 200 | } catch (error) { 201 | // easycode.ignore doesn't exist, ignore error 202 | } 203 | } 204 | 205 | for await (const entry of dirHandle.values()) { 206 | const relativePath = path ? `${path}/${entry.name}` : entry.name; 207 | 208 | await updateProgress(fileListElement, `Scanning: ${relativePath}`); 209 | 210 | // Check if path should be ignored 211 | if (shouldIgnorePath(relativePath, ignorePatterns)) { 212 | continue; 213 | } 214 | 215 | if (entry.kind === 'directory') { 216 | try { 217 | const newDirHandle = await dirHandle.getDirectoryHandle(entry.name); 218 | yield* listFilesRecursively(newDirHandle, relativePath, fileListElement, ignorePatterns); 219 | } catch (error) { 220 | console.warn(`Skipping directory ${relativePath}:`, error); 221 | } 222 | } else { 223 | yield relativePath; 224 | } 225 | } 226 | } catch (error) { 227 | console.error(`Error processing directory ${path}:`, error); 228 | throw error; // Rethrow to handle it in the calling function 229 | } 230 | } 231 | 232 | async function handleFolderSelection() { 233 | const fileListElement = document.getElementById('fileList'); 234 | fileListElement.innerHTML = '

Files in project:

'; 235 | 236 | const input = document.createElement('input'); 237 | input.type = 'file'; 238 | input.setAttribute('webkitdirectory', ''); 239 | input.setAttribute('directory', ''); 240 | input.style.display = 'none'; 241 | 242 | input.addEventListener('change', async (e) => { 243 | // FileList object contains file metadata without reading contents 244 | const files = Array.from(e.target.files); 245 | const fileList = document.createElement('ul'); 246 | fileList.style.cssText = 'list-style: none; padding: 0; max-height: 400px; overflow-y: auto;'; 247 | fileListElement.appendChild(fileList); 248 | 249 | let fileCount = 0; 250 | 251 | // Each file object has these properties without reading content: 252 | // - name: filename 253 | // - webkitRelativePath: full path relative to selected directory 254 | // - size: file size in bytes 255 | // - type: MIME type 256 | // - lastModified: timestamp 257 | 258 | for (const file of files) { 259 | // Check if file should be ignored 260 | if (shouldIgnorePath(file.webkitRelativePath, commonIgnoredPatterns)) { 261 | console.log("ignoring ", file.webkitRelativePath); 262 | continue; 263 | } 264 | 265 | const li = document.createElement('li'); 266 | li.textContent = file.webkitRelativePath; 267 | li.style.cssText = 'padding: 4px 0; border-bottom: 1px solid #eee;'; 268 | 269 | fileCount++; 270 | fileList.append(li); 271 | } 272 | 273 | // Show final count 274 | const countDiv = document.createElement('div'); 275 | countDiv.textContent = `Total files: ${fileCount}`; 276 | countDiv.style.marginTop = '10px'; 277 | fileListElement.appendChild(countDiv); 278 | 279 | 280 | const fileMetadata = files.map(file => ({ 281 | name: file.name, 282 | path: file.webkitRelativePath, 283 | size: file.size, 284 | type: file.type, 285 | lastModified: new Date(file.lastModified) 286 | })).filter(file => !shouldIgnorePath(file.path, commonIgnoredPatterns)); 287 | 288 | // Store metadata for later use 289 | chrome.storage.local.set({ 290 | projectPath: files[0]?.webkitRelativePath.split('/')[0] || '', 291 | projectLastAccessed: Date.now(), 292 | fileMetadata: fileMetadata // Store metadata for later use 293 | }); 294 | 295 | // Display files using virtual scrolling... 296 | // (rest of the display logic from previous optimized version) 297 | }); 298 | 299 | input.click(); 300 | } 301 | 302 | // Later, when you need to read a specific file's content: 303 | async function readFileContent(filePath) { 304 | const input = document.createElement('input'); 305 | input.type = 'file'; 306 | input.setAttribute('webkitdirectory', ''); 307 | 308 | return new Promise((resolve, reject) => { 309 | input.addEventListener('change', async (e) => { 310 | const files = Array.from(e.target.files); 311 | const targetFile = files.find(f => f.webkitRelativePath === filePath); 312 | 313 | if (!targetFile) { 314 | reject(new Error('File not found')); 315 | return; 316 | } 317 | 318 | try { 319 | const content = await targetFile.text(); 320 | resolve(content); 321 | } catch (error) { 322 | reject(error); 323 | } 324 | }); 325 | 326 | input.click(); 327 | }); 328 | } 329 | 330 | // Add event listener for folder selection 331 | document.getElementById('selectFolder').addEventListener('click', handleFolderSelection); 332 | 333 | // Initialize UI when document loads 334 | document.addEventListener('DOMContentLoaded', () => { 335 | const selectFolderBtn = document.getElementById('selectFolder'); 336 | selectFolderBtn.style.cssText = ` 337 | margin-top: 20px; 338 | background-color: #2196F3; 339 | color: white; 340 | padding: 8px 15px; 341 | border: none; 342 | border-radius: 4px; 343 | cursor: pointer; 344 | `; 345 | 346 | selectFolderBtn.addEventListener('mouseover', () => { 347 | selectFolderBtn.style.backgroundColor = '#1976D2'; 348 | }); 349 | selectFolderBtn.addEventListener('mouseout', () => { 350 | selectFolderBtn.style.backgroundColor = '#2196F3'; 351 | }); 352 | 353 | restoreOptions(); 354 | checkConnection(); 355 | displayCachedFiles(); // Display cached files when page loads 356 | }); 357 | 358 | // Add storage change listener to update displayed files when cache changes 359 | chrome.storage.onChanged.addListener((changes, namespace) => { 360 | if (namespace === 'local' && changes.filePaths) { 361 | displayCachedFiles(); 362 | } 363 | }); 364 | -------------------------------------------------------------------------------- /platform.js: -------------------------------------------------------------------------------- 1 | const PLATFORMS = { 2 | CHATGPT: { 3 | hostnames: ['chat.openai.com', 'chatgpt.com'], 4 | selectors: { 5 | inputFieldParent: '#composer-background', 6 | inputField: '#prompt-textarea', 7 | sendButton: '[data-testid="send-button"]', 8 | editor: '.ProseMirror', 9 | codeBlock: [ 10 | 'pre[class*="!overflow-visible"] code', 11 | '#codemirror .cm-content' 12 | ] 13 | }, 14 | inputFieldType: 'contenteditable', 15 | buttonStyle: { 16 | container: '.sticky', 17 | style: ` 18 | line-height: initial; 19 | padding: 4px 8px; 20 | background: #2A2B32; 21 | border: 1px solid #565869; 22 | border-radius: 4px; 23 | color: white; 24 | cursor: pointer; 25 | font-size: 12px; 26 | margin-right: 8px; 27 | `, 28 | classNames: '', 29 | icon: '📋 Apply' 30 | } 31 | }, 32 | CLAUDE: { 33 | hostnames: ['claude.ai'], 34 | selectors: { 35 | inputFieldParent: '.flex.flex-col.bg-bg-000', 36 | inputField: '[contenteditable="true"].ProseMirror', 37 | sendButton: 'button[aria-label="Send Message"]', 38 | editor: '.ProseMirror', 39 | codeBlock: 'div.code-block__code code' 40 | }, 41 | inputFieldType: 'contenteditable', 42 | buttonStyle: { 43 | container: '.flex.flex-1.items-center.justify-end', 44 | style: `pointer-events:auto`, 45 | classNames: `inline-flex items-center justify-center relative shrink-0 ring-offset-2 46 | ring-offset-bg-300 ring-accent-main-100 focus-visible:outline-none 47 | focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 48 | disabled:shadow-none disabled:drop-shadow-none bg-[radial-gradient(ellipse,_var(--tw-gradient-stops))] 49 | from-bg-500/10 from-50% to-bg-500/30 border-0.5 border-border-400 50 | font-medium font-styrene text-text-100/90 transition-colors 51 | active:bg-bg-500/50 hover:text-text-000 hover:bg-bg-500/60 52 | h-8 rounded-md px-3 text-xs min-w-[4rem] active:scale-[0.985] whitespace-nowrap`, 53 | icon: '📋 Apply' 54 | } 55 | } 56 | }; 57 | 58 | function getCurrentPlatform() { 59 | const currentHostname = window.location.hostname; 60 | return Object.values(PLATFORMS).find(platform => 61 | platform.hostnames.some(hostname => 62 | currentHostname.includes(hostname) 63 | ) 64 | ); 65 | } 66 | 67 | function getPlatformById(platformId) { 68 | return PLATFORMS[platformId]; 69 | } 70 | 71 | function getSelectors() { 72 | const platform = getCurrentPlatform(); 73 | return platform ? platform.selectors : null; 74 | } 75 | 76 | function getInputFieldContainer() { 77 | const container = document.querySelector(getCurrentPlatform().selectors.inputFieldParent); 78 | return container; 79 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* styles.css */ 2 | 3 | #mention-context-menu { 4 | position: absolute; 5 | height: 150px ! important; 6 | overflow-y: auto; 7 | top: calc(var(--menu-top) - 155px); 8 | z-index: 10000; 9 | background-color: #000; 10 | border: 1px solid #333; 11 | color: #fff; 12 | border-radius: 3px; 13 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 14 | font-family: Arial, sans-serif; 15 | } 16 | 17 | #mention-context-menu::-webkit-scrollbar { 18 | width: 8px; 19 | } 20 | 21 | #mention-context-menu::-webkit-scrollbar-track { 22 | background: #1a1a1a; 23 | } 24 | 25 | #mention-context-menu::-webkit-scrollbar-thumb { 26 | background: #444; 27 | border-radius: 4px; 28 | } 29 | 30 | .mention-menu-item { 31 | padding: 5px 10px; 32 | cursor: pointer; 33 | } 34 | 35 | .mention-menu-item:hover { 36 | background-color: #333; 37 | } 38 | 39 | .mention-menu-item.no-results { 40 | color: #ccc; 41 | cursor: default; 42 | } 43 | 44 | .mention-highlight { 45 | border: 1px solid #ccc; 46 | padding: 2px 4px; 47 | font-weight: bold; 48 | border-radius: 4px; 49 | } 50 | 51 | /* styles.css */ 52 | 53 | /* Add this definition for the highlighted class */ 54 | .highlighted { 55 | background-color: #555; /* Change this to a color that stands out */ 56 | color: #fff; /* Optional: change text color for better visibility */ 57 | } 58 | 59 | .menu-input { 60 | width: 100%; 61 | box-sizing: border-box; 62 | padding: 8px; 63 | border: none; 64 | border-bottom: 1px solid #ccc; 65 | outline: none; 66 | background-color: inherit ! important; 67 | } 68 | 69 | .animate-spin { 70 | animation: spin 1s linear infinite; 71 | } 72 | 73 | @keyframes spin { 74 | from { 75 | transform: rotate(0deg); 76 | } 77 | to { 78 | transform: rotate(360deg); 79 | } 80 | } 81 | 82 | .add-context-btn:hover { 83 | background-color: #444 !important; 84 | } 85 | 86 | .add-context-btn:active { 87 | transform: translateY(1px); 88 | } 89 | 90 | /* Platform specific adjustments */ 91 | #composer-background .add-context-btn { 92 | top: 5px; 93 | left: 5px; 94 | } 95 | 96 | .flex.flex-col.bg-bg-000 .add-context-btn { 97 | top: 8px; 98 | left: 8px; 99 | } 100 | 101 | .settings-link { 102 | user-select: none; 103 | } 104 | 105 | /* Platform specific adjustments */ 106 | #composer-background .settings-link { 107 | right: 8px; 108 | } 109 | 110 | .flex.flex-col.bg-bg-000 .settings-link { 111 | right: 10px; 112 | } -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | // Utility Functions 2 | 3 | /** 4 | * Tokenizes code into alphanumeric tokens. 5 | * @param {string} code - The code to tokenize. 6 | * @returns {string[]} - An array of lowercase tokens. 7 | */ 8 | function tokenizeCode(code) { 9 | const tokens = code.match(/\w+/g); 10 | if (!tokens) return []; 11 | return tokens.map(token => token.toLowerCase()); // Convert to lowercase 12 | } 13 | 14 | /** 15 | * Normalizes a vector to mimic Python's sklearn normalization. 16 | * @param {number[]} vector - The vector to normalize. 17 | * @returns {number[]} - The normalized vector. 18 | */ 19 | function normalizeVector(vector) { 20 | const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val ** 2, 0)); 21 | if (magnitude === 0) return vector.map(() => 0); // Return a zero vector 22 | return vector.map(val => val / magnitude); 23 | } 24 | 25 | /** 26 | * Computes cosine similarity between two vectors. 27 | * @param {number[]} vec1 - The first vector. 28 | * @param {number[]} vec2 - The second vector. 29 | * @returns {number} - The cosine similarity score. 30 | */ 31 | function cosineSimilarity(vec1, vec2) { 32 | const magnitude1 = Math.sqrt(vec1.reduce((sum, val) => sum + val ** 2, 0)); 33 | const magnitude2 = Math.sqrt(vec2.reduce((sum, val) => sum + val ** 2, 0)); 34 | 35 | if (magnitude1 === 0 || magnitude2 === 0) return 0; // No similarity 36 | 37 | const dotProduct = vec1.reduce((sum, val, i) => sum + val * vec2[i], 0); 38 | return dotProduct / (magnitude1 * magnitude2); 39 | } 40 | 41 | // Custom TF-IDF Class 42 | 43 | class CustomTfIdf { 44 | constructor() { 45 | this.documents = []; 46 | this.vocabulary = new Set(); 47 | this.termFrequencies = new Map(); 48 | this.inverseDocumentFrequencies = new Map(); 49 | } 50 | 51 | /** 52 | * Adds a document to the corpus. 53 | * @param {string} doc - The document text. 54 | */ 55 | addDocument(doc) { 56 | const tokens = tokenizeCode(doc); 57 | this.documents.push(tokens); 58 | const docIndex = this.documents.length - 1; 59 | 60 | const tfMap = new Map(); 61 | tokens.forEach(token => { 62 | this.vocabulary.add(token); 63 | tfMap.set(token, (tfMap.get(token) || 0) + 1); 64 | }); 65 | 66 | // Normalize TF 67 | const totalTerms = tokens.length; 68 | tfMap.forEach((count, term) => { 69 | tfMap.set(term, count / totalTerms); 70 | }); 71 | 72 | this.termFrequencies.set(docIndex, tfMap); 73 | } 74 | 75 | /** 76 | * Computes IDF for each term in the vocabulary. 77 | */ 78 | computeIdf() { 79 | const numDocuments = this.documents.length; 80 | this.vocabulary.forEach(term => { 81 | let docCount = 0; 82 | this.documents.forEach(doc => { 83 | if (doc.includes(term)) docCount += 1; 84 | }); 85 | const idf = Math.log((numDocuments + 1) / (docCount + 1)) + 1; // Smoothing 86 | this.inverseDocumentFrequencies.set(term, idf); 87 | }); 88 | } 89 | 90 | /** 91 | * Gets the TF-IDF vector for a document. 92 | * @param {number} docIndex - Index of the document. 93 | * @returns {number[]} - TF-IDF vector. 94 | */ 95 | getTfIdfVector(docIndex) { 96 | const tfMap = this.termFrequencies.get(docIndex); 97 | if (!tfMap) return []; 98 | 99 | return Array.from(this.vocabulary).map(term => { 100 | const tf = tfMap.get(term) || 0; 101 | const idf = this.inverseDocumentFrequencies.get(term) || 0; 102 | return tf * idf; 103 | }); 104 | } 105 | 106 | /** 107 | * Finalizes the TF-IDF computation by calculating IDF. 108 | */ 109 | finalize() { 110 | this.computeIdf(); 111 | } 112 | 113 | /** 114 | * Returns the size of the vocabulary. 115 | * @returns {number} - Vocabulary size. 116 | */ 117 | getVocabularySize() { 118 | return this.vocabulary.size; 119 | } 120 | } 121 | 122 | // Main Function to Find Best Match 123 | 124 | /** 125 | * Finds the best match for a code block among a set of files based on cosine similarity. 126 | * @param {string} codeBlockContent - The content of the code block. 127 | * @param {Object} files - An object containing file names as keys and file contents as values. 128 | * @returns {string} - The file name with the highest similarity score. 129 | */ 130 | function findBestMatch(codeBlockContent, files) { 131 | // Input Validation 132 | if (!codeBlockContent || !files || Object.keys(files).length === 0) { 133 | console.log("Invalid input parameters"); 134 | return ''; 135 | } 136 | 137 | // Logging Inputs 138 | //console.log("Input code block:", codeBlockContent); 139 | //console.log("Input files:", files); 140 | 141 | const tfidf = new CustomTfIdf(); 142 | const fileNames = Object.keys(files); 143 | const documentKeys = []; 144 | 145 | // Limit processing to the first 100 files to prevent performance issues 146 | fileNames.slice(0, 100).forEach(fileName => { 147 | const fileContent = files[fileName]; 148 | const fileTokens = tokenizeCode(fileContent).join(' '); 149 | tfidf.addDocument(fileTokens); 150 | documentKeys.push(fileName); 151 | }); 152 | 153 | // Add the code block as the last document 154 | tfidf.addDocument(codeBlockContent); 155 | documentKeys.push('codeBlock'); 156 | 157 | // Finalize TF-IDF (compute IDF) 158 | tfidf.finalize(); 159 | 160 | // Map document keys to indices 161 | const documentIndexMap = {}; 162 | documentKeys.forEach((key, index) => { 163 | documentIndexMap[key] = index; 164 | }); 165 | 166 | const codeBlockIndex = documentIndexMap['codeBlock']; 167 | 168 | // Get TF-IDF vector for the code block 169 | const codeBlockVector = tfidf.getTfIdfVector(codeBlockIndex); 170 | const normalizedCodeBlockVector = normalizeVector(codeBlockVector); 171 | 172 | // Compute cosine similarity for each file 173 | const similarityScores = fileNames.map(fileName => { 174 | const index = documentIndexMap[fileName]; 175 | const fileVector = tfidf.getTfIdfVector(index); 176 | const normalizedFileVector = normalizeVector(fileVector); 177 | const score = cosineSimilarity(normalizedCodeBlockVector, normalizedFileVector); 178 | return { 179 | fileName, 180 | score, 181 | }; 182 | }); 183 | 184 | // Log Similarity Scores 185 | // console.log('Similarity Scores:'); 186 | // similarityScores.forEach(({ fileName, score }) => { 187 | // console.log(`${fileName}: ${score}`); 188 | // }); 189 | 190 | // Find the best match 191 | // const bestMatch = similarityScores.reduce((best, current) => 192 | // current.score > best.score ? current : best 193 | // ); 194 | 195 | return similarityScores; 196 | } 197 | 198 | const codeBlockContent = ` 199 | import React from 'react'; 200 | import PropTypes from 'prop-types'; 201 | import { useRouteMatch } from 'react-router-dom'; 202 | import { Draggable } from 'react-beautiful-dnd'; 203 | 204 | import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components'; 205 | 206 | import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar, Checkbox } from './Styles'; 207 | 208 | const propTypes = { 209 | projectUsers: PropTypes.array.isRequired, 210 | issue: PropTypes.object.isRequired, 211 | index: PropTypes.number.isRequired, 212 | }; 213 | 214 | const ProjectBoardListIssue = ({ projectUsers, issue, index }) => { 215 | const match = useRouteMatch(); 216 | 217 | const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId)); 218 | 219 | // Handler for checkbox change 220 | const handleCheckboxChange = (e) => { 221 | console.log(\`Issue \${issue.id} checkbox changed: \`, e.target.checked); 222 | // Implement any additional logic for handling checkbox change here 223 | }; 224 | 225 | return ( 226 | 227 | {(provided, snapshot) => ( 228 | 235 | {/* Checkbox */} 236 | 237 | 242 | 243 | 244 | {/* Issue content */} 245 |
246 | {issue.title} 247 | 248 |
249 | {assignees.map(user => ( 250 | 256 | ))} 257 |
258 |
259 |
260 |
261 | )} 262 |
263 | ); 264 | }; 265 | 266 | ProjectBoardListIssue.propTypes = propTypes; 267 | 268 | export default ProjectBoardListIssue; 269 | `; 270 | 271 | const files = { 272 | "Issue.jsx": `import React from 'react'; 273 | import PropTypes from 'prop-types'; 274 | import { useRouteMatch } from 'react-router-dom'; 275 | import { Draggable } from 'react-beautiful-dnd'; 276 | 277 | import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components'; 278 | 279 | import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles'; 280 | 281 | const propTypes = { 282 | projectUsers: PropTypes.array.isRequired, 283 | issue: PropTypes.object.isRequired, 284 | index: PropTypes.number.isRequired, 285 | }; 286 | 287 | const ProjectBoardListIssue = ({ projectUsers, issue, index }) => { 288 | const match = useRouteMatch(); 289 | 290 | const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId)); 291 | 292 | return ( 293 | 294 | {(provided, snapshot) => ( 295 | 302 | 303 | {issue.title} 304 | 305 |
306 | 307 | 308 |
309 | 310 | {assignees.map(user => ( 311 | 317 | ))} 318 | 319 |
320 |
321 |
322 | )} 323 |
324 | ); 325 | }; 326 | 327 | ProjectBoardListIssue.propTypes = propTypes; 328 | 329 | export default ProjectBoardListIssue; 330 | `, // Add file content here 331 | "Board.jsx": ` 332 | import React, { Fragment } from 'react'; 333 | import PropTypes from 'prop-types'; 334 | import { Route, useRouteMatch, useHistory } from 'react-router-dom'; 335 | 336 | import useMergeState from 'shared/hooks/mergeState'; 337 | import { Breadcrumbs, Modal } from 'shared/components'; 338 | 339 | import Header from './Header'; 340 | import Filters from './Filters'; 341 | import Lists from './Lists/Lists'; 342 | import IssueDetails from './IssueDetails'; 343 | 344 | const propTypes = { 345 | project: PropTypes.object.isRequired, 346 | fetchProject: PropTypes.func.isRequired, 347 | updateLocalProjectIssues: PropTypes.func.isRequired, 348 | }; 349 | 350 | const defaultFilters = { 351 | searchTerm: '', 352 | userIds: [], 353 | myOnly: false, 354 | recent: false, 355 | }; 356 | 357 | const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => { 358 | const match = useRouteMatch(); 359 | const history = useHistory(); 360 | 361 | const [filters, mergeFilters] = useMergeState(defaultFilters); 362 | 363 | return ( 364 | 365 | 366 |
367 | 373 | 378 | ( 381 | history.push(match.url)} 387 | renderContent={modal => ( 388 | 395 | )} 396 | /> 397 | )} 398 | /> 399 | 400 | ); 401 | }; 402 | 403 | ProjectBoard.propTypes = propTypes; 404 | 405 | export default ProjectBoard; 406 | `, // Add file content here 407 | "Lists.jsx": `import React from 'react'; 408 | import PropTypes from 'prop-types'; 409 | import { DragDropContext } from 'react-beautiful-dnd'; 410 | 411 | import useCurrentUser from 'shared/hooks/currentUser'; 412 | import api from 'shared/utils/api'; 413 | import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript'; 414 | import { IssueStatus } from 'shared/constants/issues'; 415 | 416 | import List from './List/List'; 417 | import { Lists } from './Styles'; 418 | 419 | const propTypes = { 420 | project: PropTypes.object.isRequired, 421 | filters: PropTypes.object.isRequired, 422 | updateLocalProjectIssues: PropTypes.func.isRequired, 423 | }; 424 | 425 | const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => { 426 | const { currentUserId } = useCurrentUser(); 427 | 428 | const handleIssueDrop = ({ draggableId, destination, source }) => { 429 | if (!isPositionChanged(source, destination)) return; 430 | 431 | const issueId = Number(draggableId); 432 | 433 | api.optimisticUpdate(\`/issues/\${issueId}\`, { 434 | updatedFields: { 435 | status: destination.droppableId, 436 | listPosition: calculateIssueListPosition(project.issues, destination, source, issueId), 437 | }, 438 | currentFields: project.issues.find(({ id }) => id === issueId), 439 | setLocalData: fields => updateLocalProjectIssues(issueId, fields), 440 | }); 441 | }; 442 | 443 | return ( 444 | 445 | 446 | {Object.values(IssueStatus).map(status => ( 447 | 454 | ))} 455 | 456 | 457 | ); 458 | }; 459 | 460 | const isPositionChanged = (destination, source) => { 461 | if (!destination) return false; 462 | const isSameList = destination.droppableId === source.droppableId; 463 | const isSamePosition = destination.index === source.index; 464 | return !isSameList || !isSamePosition; 465 | }; 466 | 467 | const calculateIssueListPosition = (...args) => { 468 | const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args); 469 | let position; 470 | 471 | if (!prevIssue && !nextIssue) { 472 | position = 1; 473 | } else if (!prevIssue) { 474 | position = nextIssue.listPosition - 1; 475 | } else if (!nextIssue) { 476 | position = prevIssue.listPosition + 1; 477 | } else { 478 | position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2; 479 | } 480 | return position; 481 | }; 482 | 483 | const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => { 484 | const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId); 485 | const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId); 486 | const isSameList = destination.droppableId === source.droppableId; 487 | 488 | const afterDropDestinationIssues = isSameList 489 | ? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index) 490 | : insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index); 491 | 492 | return { 493 | prevIssue: afterDropDestinationIssues[destination.index - 1], 494 | nextIssue: afterDropDestinationIssues[destination.index + 1], 495 | }; 496 | }; 497 | 498 | const getSortedListIssues = (issues, status) => 499 | issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); 500 | 501 | ProjectBoardLists.propTypes = propTypes; 502 | 503 | export default ProjectBoardLists; 504 | `, // Add file content here 505 | }; 506 | 507 | // Example Usage 508 | //const bestMatch = findBestMatch(codeBlockContent, files); 509 | //console.log(`The code block most likely belongs to: ${bestMatch}`); --------------------------------------------------------------------------------