AkinO3

Discover similar stories inside works on the Archive Of Our Own.

// ==UserScript==
// @name         AkinO3
// @namespace    http://tampermonkey.net/
// @version      1.11.5
// @description  Discover similar stories inside works on the Archive Of Our Own.
// @author       dxudz
// @match        https://archiveofourown.org/works/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
  "use strict";

  // === SETTINGS with defaults ===
  const defaultSettings = {
    language: "en",
    complete: "T",
    crossover: "",
    tagBlocklist: "",
    wordsFrom: "",
    wordsTo: ""
  };

  const languageOptions = [
    { code: "all", label: "All Languages" },
    { code: "en", label: "English" },
    { code: "zh", label: "Chinese" },
    { code: "fr", label: "French" },
    { code: "de", label: "German" },
    { code: "it", label: "Italian" },
    { code: "ja", label: "Japanese" },
    { code: "ko", label: "Korean" },
    { code: "pl", label: "Polish" },
    { code: "pt", label: "Portuguese" },
    { code: "ru", label: "Russian" },
    { code: "es", label: "Spanish" },
  ];

  function loadSettings() {
    return {
      language: GM_getValue("language", defaultSettings.language),
      complete: GM_getValue("complete", defaultSettings.complete),
      crossover: GM_getValue("crossover", defaultSettings.crossover),
      tagBlocklist: GM_getValue("tagBlocklist", defaultSettings.tagBlocklist),
      wordsFrom: GM_getValue("wordsFrom", defaultSettings.wordsFrom),
      wordsTo: GM_getValue("wordsTo", defaultSettings.wordsTo),
    };
  }

  function saveSettings(settings) {
    GM_setValue("language", settings.language);
    GM_setValue("complete", settings.complete);
    GM_setValue("crossover", settings.crossover);
    GM_setValue("tagBlocklist", settings.tagBlocklist);
    GM_setValue("wordsFrom", settings.wordsFrom);
    GM_setValue("wordsTo", settings.wordsTo);
  }

  let modal, overlay;
  function createSettingsModal() {
    if (modal) return;

    overlay = document.createElement("div");
    overlay.style.position = "fixed";
    overlay.style.top = "0";
    overlay.style.left = "0";
    overlay.style.width = "100vw";
    overlay.style.height = "100vh";
    overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
    overlay.style.zIndex = "100000";
    overlay.style.display = "none";

    modal = document.createElement("div");
    modal.style.position = "fixed";
    modal.style.top = "50%";
    modal.style.left = "50%";
    modal.style.transform = "translate(-50%, -50%)";
    modal.style.backgroundColor = "#ddd";
    modal.style.padding = "1em 1.5em";
    modal.style.borderRadius = "16px";
    modal.style.boxShadow = "0 2px 12px rgba(0,0,0,0.4)";
    modal.style.zIndex = "100001";
    modal.style.minWidth = "320px";
    modal.style.maxWidth = "90vw";
    modal.style.display = "none";
    modal.style.color = "#000";
      modal.style.overflowY = "auto";
      modal.style.maxHeight = "80vh";
      modal.style.overflowY = "auto";
      modal.style.scrollbarWidth = "none"; // Firefox
      modal.style.msOverflowStyle = "none"; // IE & Edge

      const style = document.createElement("style");
style.textContent = `
  div::-webkit-scrollbar {
    display: none;
  }
`;
document.head.appendChild(style);


    const langOptionsHtml = languageOptions.map(opt => `<option value="${opt.code}">${opt.label}</option>`).join("");

   modal.innerHTML = `
  <style>
    #akinO3-settings-form select,
    #akinO3-settings-form input,
    #akinO3-settings-form textarea {
      width: 100%;
      padding: 0.5em;
      margin-bottom: 1.3em;
      border: 1px solid #ccc;
      border-radius: 30px;
      color: #000;
      font-size: 1em;
      box-sizing: border-box;
      box-shadow: inset 1px 1px 4px rgba(0,0,0,0.15);
      transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.3s ease;
      font-family: inherit;
    }

    #akinO3-settings-form select.akinO3-input {
      line-height: 1.5em;
      padding-top: 0.5em;
      padding-bottom: 0.5em;
      appearance: none;
      -webkit-appearance: none;
      -moz-appearance: none;
      background-position: right 0.7em center;
      background-repeat: no-repeat;
      background-size: 1em;
    }

    #akinO3-settings-form input:focus,
    #akinO3-settings-form textarea:focus,
    #akinO3-settings-form select:focus {
    border-color: #900;
    outline: none;
    box-shadow: 0 0 5px rgba(136, 0, 0, 0.4);
    background-color:
    }


    #akinO3-settings-form select:hover,
    #akinO3-settings-form input:hover,
    #akinO3-settings-form textarea:hover {
      border-color: #900;
      box-shadow: 0 0 5px rgba(136, 0, 0, 0.3);
      transform: scale(0.95);
    }

    #akinO3-settings-form select option {
      font-family: inherit;
      font-weight: normal;
      padding: 0.3em 0.5em;
    }

    #akinO3-settings-form label {
      display: block;
      margin-bottom: 0.4em;
      font-weight: bold;
    }

    #akinO3-settings-form h2 {
      margin-top: 0;
      margin-bottom: 0.5em;
    }

    #akinO3-settings-form p.header {
      font-size: 1em;
      font-weight: normal;
      margin: 1em 0 1em 0;
    }

    #akinO3-settings-form p.word-count-header {
      font-size: 1em;
      font-weight: normal;
      margin: 0.5em 0 1.2em 0;
    }

    #akinO3-settings-form .button-group {
      text-align: right;
      margin-top: 1em;
    }

    #akinO3-settings-form .button-group button {
      padding: 0.4em 0.9em;
      border: none;
      border-radius: 16px;
      cursor: pointer;
      font-weight: bold;
      transition: background 0.2s ease; transform 0.3s ease;
    }

    #akinO3-cancel-btn {
      background: #ccc;
      color: #000;
      transition: transform 0.3s ease;
      margin-right: 0.5em;
    }

    #akinO3-cancel-btn:hover {
      background: #bbb;
      transform: scale(0.90);
    }

    #akinO3-settings-form button[type="submit"] {
      background: #900;
      color: #fff;
      transition: transform 0.3s ease;
    }

    #akinO3-settings-form button[type="submit"]:hover {
      background: #b00;
      transform: scale(0.90);
    }

    #akinO3-settings-form .akinO3-input {
      width: 100%;
      padding: 0.5em;
      font-size: 1em;
      border: 1px solid #ccc;
      border-radius: 16px;
      box-sizing: border-box;
      margin-bottom: 1.3em;
      transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.3s ease;
      height: 2.5em;
      vertical-align: middle;
      font-family: inherit;
    }

    #akinO3-settings-form .akinO3-input:hover {
      border-color: #900;
      box-shadow: 0 0 5px rgba(136, 0, 0, 0.3);
      transform: scale(0.90);
    }
  </style>

  <h3 style="text-align: center; font-size: 2em; font-weight: bold; margin-bottom: 0;">AkinO3 Parameters</h3>
  <h5 class="header" style="text-align: center; font-size: 1.1em; margin-bottom: 1.5em;">Select your work preferences!</h5>
  <form id="akinO3-settings-form">
    <label for="akinO3-language">Language:</label>
    <select id="akinO3-language" name="language" class="akinO3-input">
      ${langOptionsHtml}
    </select>

    <label for="akinO3-complete">Completion Status:</label>
    <select id="akinO3-complete" name="complete" class="akinO3-input">
      <option value="T">Complete works only</option>
      <option value="F">Works in progress only</option>
      <option value="A">All works</option>
    </select>

    <label for="akinO3-crossover">Crossovers:</label>
    <select id="akinO3-crossover" name="crossover" class="akinO3-input">
      <option value="">Include crossovers</option>
      <option value="F">Exclude crossovers</option>
      <option value="T">Show only crossovers</option>
    </select>

    <h5 style="margin-top: 0.5em; margin-bottom: 0.5em; font-weight: normal; font-size: 1.1em;">Word Count:</h5>

    <label for="akinO3-wordsFrom">From:</label>
    <input type="number" id="akinO3-wordsFrom" name="wordsFrom" class="akinO3-input" step="1000" />

    <label for="akinO3-wordsTo">To:</label>
    <input type="number" id="akinO3-wordsTo" name="wordsTo" class="akinO3-input" step="1000" />

    <label for="akinO3-tagBlocklist">Tag blocklist (comma-separated):</label>
    <textarea id="akinO3-tagBlocklist" name="tagBlocklist" rows="3" class="akinO3-input" placeholder="e.g. major character death, rape/non-con, gore"></textarea>

    <div class="button-group">
      <button type="button" id="akinO3-cancel-btn" style="transition: transform 0.3s ease;">Close</button>
      <button type="submit">Save</button>
    </div>

    <div id="akinO3-confirmation" style="color: green; margin-top: 10px; display: none; font-weight: bold;"></div>
  </form>
`;


    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    overlay.addEventListener("click", closeModal);
    document.getElementById("akinO3-cancel-btn").addEventListener("click", closeModal);
    document.getElementById("akinO3-settings-form").addEventListener("submit", saveModalSettings);
  }

  function openSettingsModal() {
    createSettingsModal();
    const settings = loadSettings();

    modal.querySelector("#akinO3-language").value = settings.language || "all";
    modal.querySelector("#akinO3-complete").value = settings.complete || "T";
    modal.querySelector("#akinO3-crossover").value = settings.crossover || "";
    modal.querySelector("#akinO3-tagBlocklist").value = settings.tagBlocklist || "";
    modal.querySelector("#akinO3-wordsFrom").value = settings.wordsFrom || "";
    modal.querySelector("#akinO3-wordsTo").value = settings.wordsTo || "";

    modal.querySelector("#akinO3-confirmation").style.display = "none";
    modal.querySelector("#akinO3-confirmation").textContent = "";

    overlay.style.display = "block";
    modal.style.display = "block";
  }

  function closeModal() {
    if (modal) modal.style.display = "none";
    if (overlay) overlay.style.display = "none";
  }

  function saveModalSettings(e) {
    e.preventDefault();
    const form = e.target;
    const newSettings = {
      language: form.language.value.toLowerCase(),
      complete: ["T", "F", "A"].includes(form.complete.value.toUpperCase()) ? form.complete.value.toUpperCase() : "T",
      crossover: ["", "F", "T"].includes(form.crossover.value.toUpperCase()) ? form.crossover.value.toUpperCase() : "T",
      tagBlocklist: form.tagBlocklist.value.toLowerCase(),
      wordsFrom: form.wordsFrom.value.trim(),
      wordsTo: form.wordsTo.value.trim()
    };
    console.log("Saving settings:", newSettings);
    saveSettings(newSettings);

    const confirmation = modal.querySelector("#akinO3-confirmation");
    confirmation.innerHTML = "Settings saved!<br>Please run 'Randomize' to apply.";
    confirmation.style.display = "block";
    confirmation.style.color = "#000";
    confirmation.style.fontSize = "16px";
    confirmation.style.textAlign = "center";
    confirmation.style.marginTop = "16px";
  }

  GM_registerMenuCommand("AkinO3 Settings", openSettingsModal);

  function getTagText(selector) {
    return Array.from(document.querySelectorAll(selector)).map(el => el.textContent.trim());
  }

  let selectedPairing = null; // Store user's choice for the session

function createPairingModal(relationships) {
  const overlay = document.createElement("div");
  overlay.style.position = "fixed";
  overlay.style.top = "0";
  overlay.style.left = "0";
  overlay.style.width = "100%";
  overlay.style.height = "100%";
  overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
  overlay.style.zIndex = "100000";

  const modal = document.createElement("div");
  modal.style.position = "fixed";
  modal.style.top = "50%";
  modal.style.left = "50%";
  modal.style.transform = "translate(-50%, -50%)";
  modal.style.backgroundColor = "#ddd";
  modal.style.padding = "1.5em";
  modal.style.borderRadius = "16px";
  modal.style.boxShadow = "0 2px 12px rgba(0,0,0,0.4)";
  modal.style.zIndex = "100001";
  modal.style.minWidth = "300px";
  modal.style.maxWidth = "90vw";
  modal.style.color = "#000";

  modal.innerHTML = `
    <style>
      .akinO3-pairing-label {
        transition: transform 0.2s ease;
        display: block;
        margin: 1em 0;
      }
      .akinO3-pairing-label:hover {
        transform: scale(0.95);
      }
    </style>
    <h3 style="text-align: center; margin-top: 0;">What are you looking for today?</h3>
    <p style="text-align: center; color: #666; font-style: italic; margin: 0 0 1.5em 0; font-size: 0.9em;">
      Your choice lasts until you refresh the page.
    </p>
    <div class="pairing-section">
      <h4 style="margin-bottom: 0.5em; margin-top: 0.5em;">Pairings available:</h4>
      ${relationships.map(pairing => `
        <label class="akinO3-pairing-label" style="display: block; margin: 0.5em 0;">
          <input type="radio" name="pairing" value="${pairing}" style="margin-right: 0.5em;">
          ${pairing}
        </label>
      `).join("")}
    </div>
    <div style="display: flex; justify-content: center; gap: 1em; margin-top: 1.5em;">
      <button id="selection-cancel-btn" style="
        padding: 0.5em 1em;
        background: #ccc;
        color: black;
        border: none;
        border-radius: 16px;
        cursor: pointer;
        font-weight: bold;
        transition: background-color 0.2s ease, transform 0.3s ease;
      ">Cancel</button>
      <button id="selection-start-btn" style="
        padding: 0.5em 1em;
        background: #900;
        color: white;
        border: none;
        border-radius: 16px;
        cursor: pointer;
        font-weight: bold;
        transition: background-color 0.2s ease, transform 0.3s ease;
      ">Start Search</button>
    </div>
  `;

  return new Promise((resolve) => {
    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    const startBtn = modal.querySelector("#selection-start-btn");
    const cancelBtn = modal.querySelector("#selection-cancel-btn");

    // Add hover effects
    startBtn.addEventListener("mouseenter", () => {
      startBtn.style.backgroundColor = "#b00";
      startBtn.style.transform = "scale(0.95)";
    });
    startBtn.addEventListener("mouseleave", () => {
      startBtn.style.backgroundColor = "#900";
      startBtn.style.transform = "scale(1)";
    });

    cancelBtn.addEventListener("mouseenter", () => {
      cancelBtn.style.backgroundColor = "#bbb";
      cancelBtn.style.transform = "scale(0.95)";
    });
    cancelBtn.addEventListener("mouseleave", () => {
      cancelBtn.style.backgroundColor = "#ccc";
      cancelBtn.style.transform = "scale(1)";
    });

    function cleanup(selection = null) {
      overlay.remove();
      modal.remove();
      resolve(selection);
    }

    // Click outside to cancel
    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) {
        cleanup(null);
      }
    });

    // Cancel button
    cancelBtn.addEventListener("click", (e) => {
      e.preventDefault();
      cleanup(null);
    });

    // Start search button
    startBtn.addEventListener("click", (e) => {
      e.preventDefault();
      const selected = modal.querySelector('input[name="pairing"]:checked');
      cleanup(selected ? selected.value : null);
    });

    // Auto-select first option
    const firstRadio = modal.querySelector('input[name="pairing"]');
    if (firstRadio) firstRadio.checked = true;
  });
}


  function buildSearchURL({ fandom, pairing, tags }) {
    const base = "https://archiveofourown.org/works?";
    const params = new URLSearchParams();
    const settings = loadSettings();

    params.set("work_search[sort_column]", "revised_at");
    params.set("work_search[other_tag_names]", tags.join(","));

    const excludedTags = settings.tagBlocklist.split(",").map(t => t.trim()).filter(t => t.length);
    params.set("work_search[excluded_tag_names]", excludedTags.join(","));

    if (settings.crossover === "") {
      params.set("work_search[crossover]", "");
    } else if (settings.crossover === "F") {
      params.set("work_search[crossover]", "F");
    } else {
      params.set("work_search[crossover]", "T");
    }

    if (settings.complete === "T") {
      params.set("work_search[complete]", "T");
    } else if (settings.complete === "F") {
      params.set("work_search[complete]", "F");
    } else {
      params.set("work_search[complete]", "");
    }

    params.set("work_search[words_from]", settings.wordsFrom || "");
    params.set("work_search[words_to]", settings.wordsTo || "");
    params.set("work_search[query]", "");

    if (settings.language !== "all") {
      params.set("work_search[language_id]", settings.language);
    } else {
      params.delete("work_search[language_id]");
    }

    params.set("commit", "Sort and Filter");
    if (pairing) {
    params.set("tag_id", pairing.replace(/\//g, '*s*'));
  } else {
    params.set("tag_id", fandom);
  }

  return base + params.toString();
}

  function shuffle(array) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
  }

 async function fetchWorks(url) {
  try {
    const res = await fetch(url);
    if (res.status === 429) {
      // signal to the caller that AO3 is overloaded
      const err = new Error("AO3_OVERLOADED");
      err.code = 429;
      throw err;
    }
    const html = await res.text();
    const temp = document.createElement("div");
    temp.innerHTML = html;
    return temp.querySelectorAll("li.work.blurb.group");
  } catch (e) {
    // Only catch network errors, let custom errors propagate
    if (e.code === 429) throw e;
    console.error("❌ Fetch error:", e);
    return [];
  }
}

  async function fetchRecommendations(forceRefresh = false) {
  // No longer prompt for pairing here; use selectedPairing from the session
  const relationships = getTagText("dd.relationship.tags a.tag");

  // If no relationships found, exit early
  if (!relationships.length) {
    console.log("No relationships found to recommend from.");
    return;
  }

  // Use the selectedPairing for this session
  if (!selectedPairing) {
    // Should not happen, but fallback to first relationship if needed
    selectedPairing = relationships[0];
  }

  const fandoms = getTagText("dd.fandom.tags a.tag");
  const allTags = getTagText("dd.freeform.tags a.tag");

  if (!fandoms.length) {
    console.log("Not enough data to recommend.");
    return;
  }

    const fandom = fandoms[0];
    const pairing = selectedPairing;

    // Get settings and filter out blocked tags
    const settings = loadSettings();
    const blockedTags = settings.tagBlocklist
      .split(",")
      .map(t => t.trim().toLowerCase())
      .filter(t => t.length);

    // Filter tags before shuffling
    const filteredTags = allTags.filter(tag =>
      !blockedTags.includes(tag.toLowerCase())
    );

    const tags = shuffle(filteredTags);

    const currentWorkIdMatch = window.location.pathname.match(/\/works\/(\d+)/);
    const currentWorkId = currentWorkIdMatch ? currentWorkIdMatch[1] : null;

    // Changed here to try 5 tags, then fallback 4, 3, 2, 1
    const attempts = [
      tags.slice(0, 5),
      tags.slice(0, 4),
      tags.slice(0, 3),
      tags.slice(0, 2),
      tags.slice(0, 1),
    ];

    let totalDisplayed = 0;
    const displayedWorkIds = new Set();

    const existing = document.getElementById("similar-works-container");
    if (existing) existing.remove();

    const container = document.createElement("div");
    container.id = "similar-works-container";
    container.style.display = "block";
    container.style.marginTop = "2em";

    const titleRow = document.createElement("div");
    titleRow.style.display = "flex";
    titleRow.style.alignItems = "center";
    titleRow.style.justifyContent = "center";
    titleRow.style.gap = "1em";

    const randomBtn = document.createElement("button");
    randomBtn.textContent = "Randomize";
    randomBtn.style.cursor = "pointer";
    randomBtn.style.padding = "0.25em 0.75em";
    randomBtn.style.borderRadius = "0.25em";
    randomBtn.style.border = "1px solid #999";
    randomBtn.style.color = "#444";
    randomBtn.style.backgroundColor = "#eee";
    randomBtn.style.fontSize = "100%";

    randomBtn.addEventListener("mouseenter", () => {
      randomBtn.style.color = "#900";
      randomBtn.style.boxShadow = "inset 2px 2px 2px #bbb";
    });
    randomBtn.addEventListener("mouseleave", () => {
      randomBtn.style.color = "#444";
      randomBtn.style.boxShadow = "none";
    });

    randomBtn.addEventListener("click", () => {
      const existing = document.getElementById("similar-works-container");
      if (existing) existing.remove();
      fetchRecommendations(true);
    });

    const title = document.createElement("h3");
    title.textContent = "You might also enjoy:";
    title.style.textAlign = "center";

    titleRow.appendChild(randomBtn);
    titleRow.appendChild(title);
    container.appendChild(titleRow);

    function createMessage(text, tagsOnly) {
      const msg = document.createElement("div");
      msg.style.marginBottom = "5px";
      msg.style.marginTop = "5px";
      msg.style.fontStyle = "italic";
      msg.style.textAlign = "center";

      const parts = text.split(" ");
      const tagIndex = parts.findIndex(part => part.startsWith("matching"));
      const tagText = parts.slice(tagIndex + 1).join(" ");
      const preText = parts.slice(0, tagIndex + 1).join(" ");

      msg.innerHTML = `${preText} <span>${tagText}</span>`;
      return msg;
    }

      let ao3Overloaded = false;

    for (let i = 0; i < attempts.length && totalDisplayed < 5; i++) {
  const tagsToUse = attempts[i];
  if (!tagsToUse.length) continue;

  const url = buildSearchURL({ fandom, pairing, tags: tagsToUse });
  console.log(`🔗 Attempt #${i + 1}: Tags [${tagsToUse.join(", ")}]`);
  console.log(`🌐 URL used: ${url}`);

  let works;
  try {
    works = await fetchWorks(url);
  } catch (e) {
        if (e.code === 429) {
          console.warn("🚫 AO3 returned 429 Too Many Requests. Aborting further attempts.");
          ao3Overloaded = true; // Just set the flag
          break;
        } else {
          console.error("❌ Error while fetching works:", e);
          continue;
        }
      }

  if (!works.length) {
    console.log(`⚠️ Found 0 works on attempt #${i + 1}, trying next fallback...`);
    continue;
  }

  const filteredWorks = Array.from(works).filter(w => {
    let id = null;
    if (w.hasAttribute("data-work-id")) {
      id = w.getAttribute("data-work-id");
    } else {
      const link = w.querySelector("h4.heading a[href*='/works/']");
      const match = link?.href?.match(/\/works\/(\d+)/);
      if (match) id = match[1];
    }
    return id !== currentWorkId && !displayedWorkIds.has(id);
  });

  if (!filteredWorks.length) {
    console.log(`⚠️ Found 0 works after filtering current work on attempt #${i + 1}, trying next fallback...`);
    continue;
  }


      shuffle(filteredWorks);
      const toShowCount = Math.min(5 - totalDisplayed, filteredWorks.length);

      const tagLabel = tagsToUse.length === 1 ? "tag" : "tags";
      container.appendChild(createMessage(`Showing ${toShowCount} result${toShowCount > 1 ? "s" : ""} matching the ${tagLabel} ${tagsToUse.join(", ")}`));

      for (let j = 0; j < toShowCount; j++) {
        const work = filteredWorks[j];
        const clone = work.cloneNode(true);
        clone.style.border = "1px solid #ccc";
        clone.style.padding = "1em";
        clone.style.margin = "1em 0";
        clone.style.borderRadius = "6px";
        clone.style.maxWidth = "950px";
        clone.style.marginLeft = "auto";
        clone.style.marginRight = "auto";

        let workId = null;
        if (work.hasAttribute("data-work-id")) {
          workId = work.getAttribute("data-work-id");
        } else {
          const link = work.querySelector("h4.heading a[href*='/works/']");
          const match = link?.href?.match(/\/works\/(\d+)/);
          if (match) workId = match[1];
        }

        if (!workId) continue;
          displayedWorkIds.add(workId);
          const markBtn = document.createElement("button");
          markBtn.textContent = "Mark for Later";
          markBtn.style.padding = "0.25em 0.75em";
          markBtn.style.border = "1px solid #999";
          markBtn.style.borderRadius = "0.25em";
          markBtn.style.color = "#444";
          markBtn.style.cursor = "pointer";
          markBtn.style.backgroundColor = "#eee";
          markBtn.style.fontSize = "100%";
          markBtn.style.marginTop = "16px";

          markBtn.addEventListener("mouseenter", () => {
            markBtn.style.color = "#900";
            markBtn.style.boxShadow = "inset 2px 2px 2px #bbb";
          });
          markBtn.addEventListener("mouseleave", () => {
            markBtn.style.color = "#444";
            markBtn.style.boxShadow = "none";
          });

          markBtn.addEventListener("click", () => {
            window.open(`https://archiveofourown.org/works/${workId}/mark_for_later/`, "_blank", "noopener,noreferrer");
          });

          clone.appendChild(markBtn);
        container.appendChild(clone);
      }

      totalDisplayed += toShowCount;
    }

     if (ao3Overloaded) {
      const overloadMsg = document.createElement("div");
         overloadMsg.style.textAlign = "center";
      overloadMsg.style.fontSize = "18px";
      overloadMsg.style.marginTop = "1em";
      overloadMsg.style.fontStyle = "italic";
      overloadMsg.textContent =
        "AO3 seems to have gotten too many requests at this time. Please try again in a couple minutes.";
      container.appendChild(overloadMsg);

         } else if (totalDisplayed === 0) {
      const noResultsMsg = document.createElement("div");
      noResultsMsg.style.textAlign = "center";
      noResultsMsg.style.fontStyle = "italic";
      noResultsMsg.style.fontSize = "18px";
      noResultsMsg.style.marginTop = "1em";
      noResultsMsg.textContent =
        "Oops, it seems like this search brought up no results at this time. Why don't you try again?";
      container.appendChild(noResultsMsg);
    }

    let settingsBtn = document.getElementById("akinO3-settings-btn");
    if (!settingsBtn) {
      settingsBtn = document.createElement("button");
      settingsBtn.id = "akinO3-settings-btn";
      settingsBtn.textContent = "AkinO3 Parameters";
      settingsBtn.style.marginTop = "1em";
      settingsBtn.style.marginBottom = "1em";
      settingsBtn.style.marginLeft = "auto";
      settingsBtn.style.marginRight = "auto";
      settingsBtn.style.display = "block";
      settingsBtn.style.cursor = "pointer";
      settingsBtn.style.padding = "0.25em 0.75em";
      settingsBtn.style.borderRadius = "0.25em";
      settingsBtn.style.border = "1px solid #999";
      settingsBtn.style.color = "#444";
      settingsBtn.style.backgroundColor = "#eee";
      settingsBtn.style.fontSize = "100%";

      settingsBtn.addEventListener("mouseenter", () => {
        settingsBtn.style.color = "#900";
        settingsBtn.style.boxShadow = "inset 2px 2px 2px #bbb";
      });
      settingsBtn.addEventListener("mouseleave", () => {
        settingsBtn.style.color = "#444";
        settingsBtn.style.boxShadow = "none";
      });

      settingsBtn.addEventListener("click", () => {
        openSettingsModal();
      });

      container.appendChild(settingsBtn);
    }

    const workskin = document.getElementById("workskin");
    if (workskin && workskin.parentNode) {
      workskin.parentNode.insertBefore(container, workskin.nextSibling);
      console.log("✅ Inserted recommendations after fic body.");
    }
  }

  // Add this event handler for the "Find Similar Works" link
  const similarToggle = document.createElement("li");
  similarToggle.innerHTML = `<a href="#" id="similar-works-toggle">Find Similar Works</a>`;

  const navActions = document.querySelector("div.feedback ul.actions");
  if (navActions) {
    navActions.appendChild(similarToggle);
  }

  // Only show the pairing modal on the first click per page load
  // Store the user's choice for the session (until refresh)
  document.addEventListener("click", async (e) => {
    if (e.target && e.target.id === "similar-works-toggle") {
      e.preventDefault();
      const container = document.getElementById("similar-works-container");
      if (container) {
        container.style.display = container.style.display === "none" ? "block" : "none";
      } else {
        // Only prompt for pairing if not already selected this session
        if (!selectedPairing) {
          const relationships = getTagText("dd.relationship.tags a.tag");
          if (relationships.length > 1) {
            selectedPairing = await createPairingModal(relationships);
            if (!selectedPairing) {
              console.log("Selection cancelled by user");
              return;
            }
          } else if (relationships.length === 1) {
            selectedPairing = relationships[0];
          } else {
            console.log("No relationships found to recommend from.");
            return;
          }
        }
        fetchRecommendations();
      }
    }
  });

})();