AkinO3

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();