Code viewer for World: AI API Project
// Author: Sam McElligott
// Student Number: 22493902

document.write(`
<!doctype html>
<html>

<head>
  <style>
    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }

    html {
      font-family: Arial, sans-serif;
    }

    hr {
      margin: 1rem 0;
    }

    input {
      width: 100%;
      padding: 0.5rem;
      border-radius: 0.5rem;
      border: 1px solid #ccc;
    }

    input:active {
      border: 1px solid #15803d;
    }

    label {
      margin-bottom: 0.5rem;
    }

    .form-item {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: center;
      margin-bottom: 1rem;
    }

    .container {
      max-width: 200px;
    }

    .card {
      border: 1px solid #ccc;
      border-radius: 0.5rem;
      padding: 1rem;
      margin: auto;
      margin-top: 1rem;
      max-width: 400px;
      background-color: white;
    }

    button {
      padding: 0.5rem 1rem;
      border: none;
      border-radius: 0.5rem;
      background-color: #15803d;
      color: white;
      cursor: pointer;
    }

    button:disabled {
      background-color: #ccc;
      cursor: not-allowed;
    }

    textarea {
      border-radius: 0.5rem;
      padding: 0.5rem;
    }

    .hover {
      box-shadow: 0 0 8px 2px #cccccc9f;
    }

    .spread {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }

    .m-left {
      margin-left: auto;
    }

    .grid {
      width: 100%;
      display: flex;
      justify-content: center;
      align-items: flex-start;
      gap: 2rem;
    }

    .grid .card {
      margin: 0;
      max-width: 100%;
    }

    @media (max-width: 768px) {
      .grid {
        flex-direction: column;
      }
    }

    .responses {
      margin-top: 1rem;
      width: 100%;
    }
  </style>
</head>

<body>
  <form class="card hover" id="key-form">
    <div class="form-item">
      <label for="chatgpt-key"> ChatGPT Key </label>
      <input id="chatgpt-key" required name="chatgpt" placeholder="Enter key here" />
    </div>
    <div class="form-item">
      <label for="gemini-key"> Gemini Key </label>
      <input id="gemini-key" required name="gemini" placeholder="Enter key here" />
    </div>
    <hr />
    <div class="form-item">
      <label for="one-compiler-key"> OneCompiler Key </label>
      <input id="one-compiler-key" required name="one-compiler" placeholder="Enter key here" />
    </div>

    <div class="form-item">
      <button type="submit" class="m-left">Submit</button>
    </div>
  </form>
  <div class="responses"></div>
</body>

</html>

`);

// Manages the form for the API keys, and manages
// storing them in local storage for ease of use
class KeyManager {
  constructor() {
    this.form = document.querySelector("#key-form");
    this.values = {
      chatGPT: null,
      gemini: null,
      oneCompiler: null,
    };
    this.ready = false;
  }

  init() {
    this.form.onsubmit = (e) => {
      e.preventDefault();

      keys.setKeys(e.target);

      if (!keys.ready) {
        alert("Please fill in all fields");
        return;
      }

      showPrompt();
    };

    const stored = this.getStoredKeys();
    if (!stored) return;

    this.values = stored;
    this.ready = this.validateKeys();

    if (this.ready) {
      showPrompt();
    }
  }

  setKeys(formObject) {
    this.values.chatGPT = formObject["chatgpt"].value;
    this.values.gemini = formObject["gemini"].value;
    this.values.oneCompiler = formObject["one-compiler"].value;

    this.ready = this.validateKeys();

    if (this.ready) {
      this.storeKeys();
    }
  }

  storeKeys() {
    localStorage.setItem("keys", JSON.stringify(this.values));
  }

  getStoredKeys() {
    const keys = localStorage.getItem("keys");

    return keys ? JSON.parse(keys) : null;
  }

  validateKeys() {
    for (const key in this.values) {
      if (this.values[key] === "") {
        return false;
      }
    }
    return true;
  }

  clearKeys() {
    localStorage.removeItem("keys");
    window.location.reload();
  }
}

// Manages the form for the code generation prompt,
// including calling the APIs
class PromptManager {
  constructor(keys) {
    this.keys = keys;
    this.prompt = null;
    this.ready = false;
    this.responses = {
      ChatGPT: null,
      Gemini: null,
      lLama: null,
      oneCompiler: null,
    };
  }

  init() {
    this.form = document.querySelector("#prompt-form");

    this.form.onsubmit = async (e) => {
      e.preventDefault();

      this.prompt = e.target["prompt"].value;
      this.ready = this.prompt !== "";

      if (!this.ready) {
        alert("Please fill in all fields");
        return;
      }

      const loading = document.createElement("h1");
      loading.id = "loading";
      loading.innerHTML = "Loading...";
      loading.style.textAlign = "center";
      document.querySelector(".responses").prepend(loading);

      // Reset form
      handleInput("");
      this.form["prompt"].value = "";
      document.querySelector("#prompt-form button[type='submit']").disabled =
        true;

      await this.generateResponses();
      this.showResponses();
    };
  }

  // When awaited, this method pauses the main thread
  // for 'delay' milliseconds
  async wait(delay) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, delay);
    });
  }

  async generateResponses() {
    // Code generation requests
    const [ChatGPT, Gemini] = await Promise.all([
      this.fetchChatGPT(),
      this.fetchGemini(),
    ]);

    // MH edit
    //console.log ("ChatGPT:"); console.log(ChatGPT);
    //console.log ("Gemini:"); console.log(Gemini);
 
    // Code running requests
    const [ChatGPTSuccess, GeminiSuccess] = await Promise.all([
      this.runCode(ChatGPT, "ChatGPT", { skip: ChatGPT.error }),
      this.runCode(Gemini, "Gemini", { skip: Gemini.error, delay: 500 }),
    ]);

    // MH edit
    //console.log ("ChatGPTSuccess:"); console.log(ChatGPTSuccess);
    //console.log ("GeminiSuccess:"); console.log(GeminiSuccess);
    
    this.responses = {
      ChatGPT: {
        code: ChatGPT,
        success: ChatGPTSuccess[0],
        time: ChatGPTSuccess[1],
      },
      Gemini: {
        code: Gemini,
        success: GeminiSuccess[0],
        time: GeminiSuccess[1],
      },
    };
  }

  // Once the responses have been generated, display them on the page in code blocks
  showResponses() {
    document.querySelector("#loading").remove();
    const container = document.querySelector(".responses");

    const heading = document.createElement("h1");
    heading.innerHTML = `"<i>${this.prompt}</i>"`;
    heading.style.textAlign = "center";

    const responseContainer = document.createElement("div");
    responseContainer.classList.add("grid");

    // MH edit
    //console.log ("responses:"); console.log (this.responses);
    
    for (const key in this.responses) {
      const { success, code, time } = this.responses[key];
      const color = success ? "green" : "red";

      const response = document.createElement("div");

      response.classList.add("card", "container");
      response.innerHTML = `<h1 style="color: ${color};">${key}</h1>
<p>Took <b>${time}</b>ms to run</p>
<hr />
<pre>
<code class="language-python" id=${key}>${code}</code>
</pre>
<button style="margin-top: 1rem;" onclick="handleCopy('${key}')">Copy</button>
`;

      responseContainer.appendChild(response);
    }

    container.prepend(responseContainer);
    container.prepend(heading);
    container.prepend(document.createElement("hr"));
  }

  async fetchChatGPT() {
    const body = {
      model: "gpt-4o-mini",
      messages: [
        {
          role: "system",
          content:
            "You must generate only code, which must be in Python and contain test cases. Don't include markdown or comments.",
        },
        {
          role: "user",
          content: this.prompt,
        },
      ],
    };

    const response = await this.fetch({
      url: "https://api.openai.com/v1/chat/completions",
      headers: {
        Authorization: `Bearer ${this.keys.values.chatGPT} `,
        
        // MH edit
        //"OpenAI-Organization": "org-WS419FrNJEV9muDDrmXw2AXd",
      },
      body,
    });

    console.log({ chat: response });

    return response.data.choices[0].message.content.trim() + "\n";
  }

  async fetchGemini() {
    const body = {
      contents: [
        {
          parts: [
            {
              text: `You must generate only code, which must be in Python and contain test cases. Don't include markdown or comments: ${this.prompt}`,
            },
          ],
        },
      ],
    };

    const result = await this.fetch({
      url: `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${this.keys.values.gemini}`,
      body,
    });

    console.log({ gemini: result });

    if (result.error) {
      return result.message;
    }

    const code = result.data.candidates[0].content.parts[0].text;

    return code.replace("```python\n", "").replace("\n```", "").trim() + "\n";
  }

  // Helper function to send a POST request
  async fetch({ url, headers, body }) {
    try {
      const result = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...headers,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(10000),
      });

      if (!result.ok) {
        const data = await result.json();
        console.log(data);
        throw new Error("Request failed: " + data.error.message);
      }

      const data = await result.json();
      return { error: false, data };
    } catch (e) {
      if (e.name === "AbortError") {
        return { error: true, message: "Request timed out (10s)" };
      }
      return { error: true, message: e.message };
    }
  }

  // Send a request to the onecompiler api to execute each code snippet
  async runCode(code, name, { skip = false, delay = 0 }) {
    if (skip) return [false, 0];

    // Delay introduced because the onecompiler api complains if
    // you send back to back requests
    await this.wait(delay);

    const response = await fetch(
      "https://onecompiler-apis.p.rapidapi.com/api/v1/run",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-rapidapi-key": this.keys.values.oneCompiler,
          "x-rapidapi-host": "onecompiler-apis.p.rapidapi.com",
        },
        body: JSON.stringify({
          language: "python",
          files: [
            {
              name,
              content: code,
            },
          ],
        }),
      },
    );

    const data = await response.json();

    // MH edit
    console.log (data.stderr);
    
    return [data.stderr === null, data.executionTime];
  }
}

const keys = new KeyManager();
const codePrompt = new PromptManager(keys);

keys.init();

// Show the prompt form after keys have been entered
function showPrompt() {
  const keyForm = document.querySelector("#key-form");
  keyForm.innerHTML = "";

  keyForm.id = "prompt-form";

  keyForm.innerHTML = `
      <div class="form-item">
      <label for="prompt">Prompt</label>
      <textarea
        id="prompt"
        oninput="handleInput(this.value)"
        name="prompt"
        required
        placeholder="Enter a code generation prompt (Max 200 chars)"
        maxlength="200"
        cols="40"
        rows="5"
      ></textarea>
      <p id="counter">0/200</p>
      <div class="spread">
        <button onclick="keys.clearKeys()" style="background: transparent; color: red; border: 1px solid red">Clear stored keys</button>
        <button type="submit" disabled>Generate Code</button>
      </div>
    </div >
      `;

  codePrompt.init();
}

// Update the character counter and enable the submit button on the prompt form
function handleInput(value) {
  const button = document.querySelector("#prompt-form button[type='submit']");
  const counter = document.querySelector("#counter");

  button.disabled = false;

  if (value === "") {
    counter.innerHTML = "0/200";
    return;
  }

  button.disabled = false;

  if (value.length > 200) {
    value = value.slice(0, 200);
  }

  counter.innerHTML = `${value.length}/200`;
}

function handleCopy(key) {
  const code = codePrompt.responses[key].code;
  navigator.clipboard.writeText(code);
  alert("Code copied to clipboard");
}