Search code examples
javascriptjsonartificial-intelligencefirefox-addon

Why does my Firefox extension to add an upload-file button only work on one chat page?


I am trying to make an extension for FireFox/Gecko-based browsers that add an upload-file button to chat.openai.com. When I add my extension, it'll only add the button when I refresh on 1 chat page. If I go to a past chat, it won't put in the button. (BTW, I wrote this code with the help of ChatGPT lol).

manifest.json:

{
    "manifest_version": 3,
    "name": "ChatGPT File Upload",
    "version": "1.0",
    "description": "Adds a button to upload files into ChatGPT. (NOT for images, videos, Word Documents, or other non-raw-text files. Please use .txt, .js, .py, .html, .css, .json, and .csv.",
    "permissions": [
      "scripting",
      "https://chat.openai.com/*"
    ],
    "action": {
      "default_icon": {
        "128": "icon128.png",
        "256": "icon128.png"
      }
    },
    "icons": {
      "128": "icon128.png",
      "256": "icon256.png"
    },
    "content_scripts": [
      {
        "matches": ["https://chat.openai.com/*"],
        "js": ["content.js"]
      }
    ],
    "background": {
      "scripts": ["background.js"],
      "service_worker": "background.js"
    }
  }  

background.js:

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.url && changeInfo.url.startsWith('https://chat.openai.com/')) {
      chrome.scripting.executeScript({
        target: { tabId: tabId },
        files: ['content.js']
      });
    }
  });
  

content.js:

console.log("Content script loaded.");

// This script will be injected into chat.openai.com pages
// You can add your desired functionality here
// Create the button
const button = document.createElement('button');
button.innerText = '📂 Submit File';
button.style.backgroundColor = '#35393d';
button.style.color = 'white';
button.style.padding = '5px';
button.style.border = '1px solid #6b6458';
button.style.borderRadius = '5px';
button.style.margin = '5px';
button.style.width = '180px';

// Create a container div for centering
const containerDiv = document.createElement('div');
containerDiv.style.display = 'flex';
containerDiv.style.justifyContent = 'center';

// Append the button to the container div
containerDiv.appendChild(button);

// Find the target element
const targetElement = document.querySelector("div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.bottom-0 > form > div > div:nth-child(1)");

// Insert the container div before the target element
targetElement.parentNode.insertBefore(containerDiv, targetElement);

// Add click event listener to the button
button.addEventListener('click', async () => {
  // Create the file input element
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.accept = '.txt, .js, .py, .html, .css, .json, .csv';

  // Handle file selection
  fileInput.addEventListener('change', async (event) => {
    const file = event.target.files[0];
    if (file) {
      const reader = new FileReader();

      reader.onload = async (e) => {
        const fileContent = e.target.result;
        const chunkSize = 15000;
        const chunks = [];

        // Split file content into chunks
        for (let i = 0; i < fileContent.length; i += chunkSize) {
          const chunk = fileContent.slice(i, i + chunkSize);
          chunks.push(chunk);
        }

        // Submit each chunk to the conversation
        for (let i = 0; i < chunks.length; i++) {
          const chunk = chunks[i];
          const part = i + 1;
          const filename = file.name;
          await submitConversation(chunk, part, filename);
        }
      };

      reader.readAsText(file);
    }
  });

  // Trigger file input click event
  fileInput.click();
});

// Submit conversation function
async function submitConversation(text, part, filename) {
  const textarea = document.querySelector("textarea[tabindex='0']");
  const enterKeyEvent = new KeyboardEvent('keydown', {
    bubbles: true,
    cancelable: true,
    keyCode: 13,
  });
  textarea.value = `Part ${part} of ${filename}:\n\n${text}`;
  textarea.dispatchEvent(enterKeyEvent);
}

I gone through different background.js's that I found online, but none seemed to solve my issue. I'm very new to development, so I'm mostly lost on this sort of thing.


Solution

  • Expanding my comment here.

    There can be two types of navigations

    1. Full page refresh
    2. Javascript based page load

    In the first case the extension will do its job and load the button but in the second case if the web app is loading the whole page with new elements then your "edited" element will get replaced with a new element.

    You can solve this in two ways (AFAIK)

    1. Listening to navigation changes
    2. Mutation Observer

    Listening to navigation Event

    If the web app is changing the address in address bar then you can listen to that event and add the button if not already present. https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event

    navigation.addEventListener("navigate", (event) => {
      checkAndInjectButton()
    })
    

    Mutation Observer

    If for some reason you are not able to detect navigation change in web app then you can listen to the changes in DOM and react based on the event.

    Mutation oberver keeps track of attributes, child node and sub tree so if any changes are made to the target element, your script will get a callback.

    // Select the node that will be observed for mutations
    const targetElement = document.querySelector("div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.bottom-0 > form > div > div:nth-child(1)");
    
    // Options for the observer (which mutations to observe)
    const config = { attributes: true, childList: true, subtree: true };
    
    // Callback function to execute when mutations are observed
    const callback = (mutationList, observer) => {
      checkAndInjectButton();
    };
    
    // Create an observer instance linked to the callback function
    const observer = new MutationObserver(callback);
    
    // Start observing the target node for configured mutations
    observer.observe(targetElement, config);
    
    // If you need to stop observing
    observer.disconnect();
    

    Both the solutions will go to your content.js