StackOverflow Code Language Switcher

Adds a dropdown to code blocks in StackOverflow to switch the code language. Powered by GPT-3 AI.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        StackOverflow Code Language Switcher
// @namespace   https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
// @homepageURL https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
// @supportURL  https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
// @description Adds a dropdown to code blocks in StackOverflow to switch the code language. Powered by GPT-3 AI.
// @match       https://stackoverflow.com/questions/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addValueChangeListener
// @grant       GM_setClipboard
// @version     1.0
// @author      Chris Hayes
// @license     GPL3
// ==/UserScript==

class LangConvert {
  constructor() {
    this.key = GM_getValue("openai_key", "");

    if (!this.key || this.key === "your-openai-key-here") {
      GM_setValue("openai_key", "your-openai-key-here");
      console.error(
        '"openai_key" not set in violent monkey script. Please get an OpenAI key and set this config value to use the OpenAI API.'
      );
    }

    this.supportedLanguages = GM_getValue("supported_languages", [
      "javascript",
      "typescript",
      "python",
      "java",
      "bash",
      "css",
      "scss",
      "html",
      "c",
      "c++",
      "c#",
      "rust",
      "go",
      "kotlin",
      "php",
      "ruby",
      "liquid",
      "swift",
      "react",
      "vue",
      "svelte",
      "angular",
    ]);

    GM_setValue("supported_languages", this.supportedLanguages);

    this.previousConversions = {};
  }

  setKey(newKey) {
    this.key = newKey;
  }
  setSupportedLanguages(newLanguages) {
    this.supportedLanguages = newLanguages;
  }

  async makeGPT3Request(code, oldLang, newLang) {
    const url = "https://api.openai.com/v1/completions";
    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${this.key}`,
    };

    // Uses Davinci 003 text-completion
    // code-completion and edit were slower and didn't work as well
    const body = {
      model: "text-davinci-003",
      prompt: `# Convert this${oldLang ? " from " + oldLang : ""} to ${newLang}
# ${oldLang ? oldLang : "Old"} version

${code}

# ${newLang} version`,
      max_tokens: 1000,
    };

    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body),
    });
    console.log(
      "[StackOverflow Code Language Switcher] Full OpenAI response:",
      response
    );

    const data = await response.json();
    console.log("[StackOverflow Code Language Switcher] Response data:", data);

    let newCode = data.choices[0].text;
    // if the first line is an empty newline, remove it
    if (newCode.startsWith("\n")) {
      newCode = newCode.slice(1);
    }
    // if the last line is an empty newline, remove it
    if (newCode.endsWith("\n")) {
      newCode = newCode.slice(0, -1);
    }

    return newCode;
  }

  getCodeElements() {
    return Array.from(document.querySelectorAll(".s-prose pre code"));
  }

  setup() {
    const codeElements = this.getCodeElements();

    for (const codeElem of codeElements) {
      const pre = codeElem.parentElement;
      const div = document.createElement("div");
      div.classList.add("lang-convert");
      div.style = `
        height: 0;
        float: right;
        margin-bottom: -20px;
        display: block;
        position: relative;
        top: 10px;
        right: 8px;
      `;

      // Show supported languages in dropdown
      const select = document.createElement("select");
      select.innerHTML = `${this.supportedLanguages
        .slice(0, 20)
        .map((lang) => `<option value="${lang}">${lang}</option>`)
        .join("")}`;
      select.style = `
        background: #181818;
        color: #999;
        outline: none;
        border: 0;
        padding: .5em 0.7em;
        border-radius: 7px;
        font-size: 12px;
      `;
      // Change style of select element when hovered over
      select.addEventListener("mouseenter", () => {
        select.style.background = "black";
        select.style.color = "white";
      });
      select.addEventListener("mouseleave", () => {
        select.style.background = "#181818";
        select.style.color = "#999";
      });
      div.appendChild(select);

      // Add div before pre element
      pre.parentElement.insertBefore(div, pre);

      // Set default value of dropdown to the language of the code element
      const lang = codeElem.className.split("-")[1];
      select.value = lang;

      this.previousConversions[lang] = codeElem.textContent;

      select.addEventListener("change", async (e) => {
        const oldLang = codeElem.className.split("-")[1];
        const newLang = e.target.value;
        if (oldLang === newLang) return;

        // Show "converting" overlay message
        const overlay = document.createElement("div");
        overlay.style = `
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          background: rgba(0, 0, 0, 0.5);
          display: flex;
          justify-content: center;
          align-items: center;
          color: white;
          font-size: 20px;
          font-weight: bold;
        `;
        overlay.innerHTML = "Converting...";
        pre.style.position = "relative";
        pre.appendChild(overlay);

        const oldCode = codeElem.textContent;
        let newCode = "";
        if (this.previousConversions[newLang]) {
          newCode = this.previousConversions[newLang];
        } else {
          newCode = await this.makeGPT3Request(oldCode, oldLang, newLang);
          this.previousConversions[newLang] = newCode;
        }

        GM_setClipboard(newCode);

        codeElem.classList.remove(`language-${oldLang}`);
        codeElem.classList.add(`language-${newLang}`);
        codeElem.textContent = newCode;

        // add a script to the page that will highlight the new code using window.hljs
        // This must be injected to have access to the highlight.js object already loaded into StackOverflow
        const codeID =
          Math.random().toString(36).substring(2, 15) +
          Math.random().toString(36).substring(2, 15);
        codeElem.id = codeID;
        const script = document.createElement("script");
        script.innerHTML = `if (window.hljs) window.hljs.highlightElement(document.getElementById("${codeID}"));`;
        document.body.appendChild(script);

        // Remove "converting" overlay message
        pre.removeChild(overlay);
        pre.style.position = "static";
      });
    }
  }
}

let langConvert = new LangConvert();
langConvert.setup();

GM_addValueChangeListener("openai_key", (name, oldValue, newValue) => {
  langConvert.setKey(newValue);
});

GM_addValueChangeListener("supported_languages", (name, oldValue, newValue) => {
  langConvert.setSupportedLanguages(newValue);
});