StackOverflow Code Language Switcher

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

Від 08.01.2023. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
});