User:Overandoutnerd/Scripts/articleSummary.js

// ==UserScript==
// @name         Wikipedia AI Summary (Google Gemini)
// @namespace    wiki-ai-summary-gemini
// @version      1.3.0
// @description  Generate AI-based summaries of Wikipedia articles for private reading
// @match        https://*.wikipedia.org/wiki/*
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  /* ------------------------------------------------------------------
   * CONFIG
   * ------------------------------------------------------------------ */

  const MODEL = "gemini-2.5-flash";
  const STORAGE_KEY = "gemini_api_key";
  const MAX_CHARS = 6000;

  /* ------------------------------------------------------------------
   * DARK MODE
   * ------------------------------------------------------------------ */

  function isDarkMode() {
    if (
      document.documentElement.classList.contains("skin-theme-clientpref-night") ||
      document.body.classList.contains("skin-theme-clientpref-night")
    ) return true;

    if (
      (
        document.documentElement.classList.contains("skin-theme-clientpref-os") ||
        document.body.classList.contains("skin-theme-clientpref-os")
      ) &&
      window.matchMedia("(prefers-color-scheme: dark)").matches
    ) return true;

    return false;
  }

  /* ------------------------------------------------------------------
   * API KEY STORAGE
   * ------------------------------------------------------------------ */

  function getApiKey() {
    return localStorage.getItem(STORAGE_KEY) || "";
  }

  function saveApiKey(key) {
    localStorage.setItem(STORAGE_KEY, key.trim());
  }

  function clearApiKey() {
    localStorage.removeItem(STORAGE_KEY);
  }

  /* ------------------------------------------------------------------
   * MARKDOWN → HTML (SAFE + BASIC)
   * ------------------------------------------------------------------ */

  function renderMarkdown(markdown) {
    let html = markdown
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");

    html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
    html = html.replace(/^\s*[-*]\s+(.*)$/gm, "<li>$1</li>");
    html = html.replace(/(<li>.*<\/li>)/gs, "<ul>$1</ul>");

    html = html
      .split(/\n{2,}/)
      .map(block => {
        if (block.startsWith("<ul>")) return block;
        return `<p>${block.replace(/\n/g, "<br>")}</p>`;
      })
      .join("");

    return html;
  }

  /* ------------------------------------------------------------------
   * WIKIPEDIA HELPERS
   * ------------------------------------------------------------------ */

  function getPageTitle() {
    return mw.config.get("wgPageName").replace(/_/g, " ");
  }

  async function fetchArticleText() {
    const url = new URL("/w/api.php", location.origin);
    url.search = new URLSearchParams({
      action: "query",
      prop: "extracts",
      explaintext: "1",
      redirects: "1",
      titles: getPageTitle(),
      format: "json"
    });

    const res = await fetch(url);
    const json = await res.json();
    return Object.values(json.query.pages)[0].extract || "";
  }

  function chunkText(text, size) {
    const chunks = [];
    for (let i = 0; i < text.length; i += size) {
      chunks.push(text.slice(i, i + size));
    }
    return chunks;
  }

  /* ------------------------------------------------------------------
   * GEMINI API
   * ------------------------------------------------------------------ */

  async function callGemini(prompt, apiKey) {
    const endpoint =
      `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${apiKey}`;

    const res = await fetch(endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        contents: [{ parts: [{ text: prompt }] }]
      })
    });

    if (!res.ok) {
      throw new Error(`Gemini API error ${res.status}`);
    }

    const json = await res.json();
    return json.candidates?.[0]?.content?.parts?.[0]?.text || "";
  }

  /* ------------------------------------------------------------------
   * STYLES
   * ------------------------------------------------------------------ */

  function injectStyles() {
    if (document.getElementById("ai-summary-styles")) return;

    const dark = isDarkMode();
    const style = document.createElement("style");
    style.id = "ai-summary-styles";

    style.textContent = `
      .ai-summary-box {
  margin: 18px 0;
  padding: 16px;
  border-radius: 14px;
  background:
    ${dark
      ? "linear-gradient(#202124, #202124) padding-box"
      : "linear-gradient(#f8f9fa, #f8f9fa) padding-box"},
    linear-gradient(135deg, #7c4dff, #00bcd4) border-box;
  border: 2px solid transparent;
  box-shadow: ${dark
    ? "0 6px 18px rgba(0,0,0,.6)"
    : "0 6px 18px rgba(0,0,0,.08)"};
  color: ${dark ? "#e8eaed" : "#202124"};
}
.ai-summary-box *,
.ai-summary-box *::before,
.ai-summary-box *::after {
  box-sizing: border-box;
}

      .ai-summary-btn {
        border: none;
        border-radius: 999px;
        padding: 8px 16px;
        font-size: 13px;
        font-weight: 600;
        color: #fff;
        background: linear-gradient(135deg, #7c4dff, #00bcd4);
        cursor: pointer;
      }

      .ai-summary-output {
        margin-top: 12px;
        font-size: 14px;
        line-height: 1.5;
      }

      .ai-summary-output p { margin: 6px 0; }
      .ai-summary-output ul { margin: 10px 0; padding-left: 20px; }
      .ai-summary-output li + li { margin-top: 5px; }

      .ai-summary-loader {
        margin-top: 12px;
        display: none;
      }

      .ai-summary-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: linear-gradient(135deg, #7c4dff, #00bcd4);
        animation: bounce 1.4s infinite ease-in-out both;
        display: inline-block;
      }

      .ai-summary-dot:nth-child(1) { animation-delay: -0.32s; }
      .ai-summary-dot:nth-child(2) { animation-delay: -0.16s; }

      @keyframes bounce {
        0%,80%,100% { transform: scale(0); opacity: .3; }
        40% { transform: scale(1); opacity: 1; }
      }
    `;

    document.head.appendChild(style);
  }

  /* ------------------------------------------------------------------
   * UI
   * ------------------------------------------------------------------ */

  function createUi() {
    injectStyles();

    const hasKey = !!getApiKey();
    const box = document.createElement("div");
    box.className = "ai-summary-box";

    box.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center">
        <strong>AI Summary</strong>
        <button
  id="edit-key-btn"
  title="Edit API key"
  aria-label="Edit API key"
  style="
    display:${hasKey ? "flex" : "none"};
    align-items:center;
    justify-content:center;
    background:none;
    border:none;
    padding:6px;
    cursor:pointer;
    opacity:.7;
    color:inherit;
  "
>
  <svg
  viewBox="0 0 24 24"
  width="16"
  height="16"
  aria-hidden="true"
  focusable="false"
>
  <path
    d="M15.673,3.913C16.892,2.694 18.868,2.694 20.087,3.913C21.306,5.132 21.306,7.108 20.087,8.327L14.15,14.264C13.385,15.029 12.392,15.525 11.321,15.678L9.141,15.99C8.83,16.034 8.515,15.929 8.293,15.707C8.07,15.484 7.966,15.17 8.01,14.858L8.321,12.678C8.474,11.607 8.971,10.615 9.736,9.85L15.673,3.913ZM18.673,5.327C18.235,4.889 17.525,4.889 17.087,5.327L11.15,11.264C10.691,11.723 10.393,12.319 10.301,12.961L10.179,13.821L11.039,13.698C11.681,13.607 12.277,13.309 12.736,12.85L18.673,6.913C19.111,6.475 19.111,5.765 18.673,5.327ZM11,3.999C11,4.551 10.553,5 10.001,5C9.002,5.001 8.298,5.008 7.747,5.061C7.207,5.112 6.885,5.201 6.638,5.327C6.074,5.614 5.615,6.073 5.327,6.638C5.193,6.901 5.101,7.249 5.051,7.854C5.001,8.471 5,9.263 5,10.4V13.6C5,14.736 5.001,15.529 5.051,16.146C5.101,16.751 5.193,17.098 5.327,17.362C5.615,17.926 6.074,18.385 6.638,18.673C6.901,18.807 7.249,18.899 7.854,18.949C8.471,18.999 9.263,19 10.4,19H13.6C14.737,19 15.529,18.999 16.146,18.949C16.751,18.899 17.099,18.807 17.362,18.673C17.927,18.385 18.385,17.926 18.673,17.362C18.799,17.115 18.888,16.793 18.939,16.253C18.992,15.702 18.999,14.998 19,13.999C19,13.447 19.448,12.999 20.001,13C20.553,13 21,13.448 21,14.001C20.999,14.979 20.993,15.781 20.93,16.442C20.866,17.116 20.739,17.713 20.455,18.27C19.976,19.211 19.211,19.976 18.27,20.455C17.678,20.757 17.038,20.882 16.309,20.942C15.601,21 14.727,21 13.643,21H10.357C9.273,21 8.399,21 7.691,20.942C6.963,20.882 6.322,20.757 5.73,20.455C4.789,19.976 4.024,19.211 3.545,18.27C3.243,17.677 3.117,17.037 3.058,16.309C3,15.601 3,14.726 3,13.643V10.357C3,9.273 3,8.399 3.058,7.691C3.117,6.962 3.243,6.322 3.545,5.73C4.024,4.789 4.789,4.024 5.73,3.545C6.286,3.261 6.884,3.133 7.557,3.069C8.219,3.007 9.021,3.001 9.999,3C10.552,3 11,3.447 11,3.999Z"
    fill="currentColor"
  />
</svg>
</button>
      </div>

      <p style="font-size:12px;opacity:.7">Generated with Google Gemini.</p>

      <div id="key-section" style="display:${hasKey ? "none" : "block"}">
        <input
  id="api-key-input"
  type="text"
  placeholder="Paste your Gemini API key"
  value="${hasKey ? getApiKey() : ""}"
  spellcheck="false"
  autocomplete="off"
  style="
    width:100%;
	max-width: 100%;
    padding:8px 10px;
    border-radius:8px;
    border:1px solid #ccc;
    font-size:13px;
  "
/>
        <button id="save-key-btn" class="ai-summary-btn" style="margin-top:8px">
          Save API Key
        </button>
      </div>

      <button id="generate-btn" class="ai-summary-btn" style="margin-top:12px">
        Generate AI Summary
      </button>

      <div id="loader" class="ai-summary-loader">
        <span class="ai-summary-dot"></span>
        <span class="ai-summary-dot"></span>
        <span class="ai-summary-dot"></span>
      </div>

      <div id="output" class="ai-summary-output"></div>
    `;

    return box;
  }

  /* ------------------------------------------------------------------
   * SUMMARY LOGIC
   * ------------------------------------------------------------------ */

  async function generateSummary() {
    const output = document.getElementById("output");
    const loader = document.getElementById("loader");
    const btn = document.getElementById("generate-btn");

    const apiKey = getApiKey();
    if (!apiKey) {
      output.textContent = "Add your Gemini API key first.";
      return;
    }

    output.textContent = "";
    loader.style.display = "block";
    btn.disabled = true;

    try {
      const text = await fetchArticleText();
      const chunks = chunkText(text, MAX_CHARS);
      const partials = [];

      for (const chunk of chunks) {
        const prompt = `
Create bullet-point notes from the text below.

Rules:
- Maximum 4 bullet points
- One sentence per bullet
- Neutral, factual tone
- No repetition

TEXT:
${chunk}
        `.trim();

        partials.push(await callGemini(prompt, apiKey));
      }

      const finalPrompt = `
Create a concise Wikipedia-style summary.

Format:
- One short paragraph (2–3 sentences)
- 3–5 bullet points

Rules:
- Neutral
- No repetition
- No new info
- Max 120 words

CONTENT:
${partials.join("\n")}
      `.trim();

      const markdown = await callGemini(finalPrompt, apiKey);
      output.innerHTML = renderMarkdown(markdown);
    } catch (err) {
      console.error(err);
      output.textContent = "Failed to generate summary.";
    } finally {
      loader.style.display = "none";
      btn.disabled = false;
    }
  }

  /* ------------------------------------------------------------------
   * INIT
   * ------------------------------------------------------------------ */

  function init() {
    const heading = document.getElementById("firstHeading");
    if (!heading) return;

    const siteSub = document.getElementById("siteSub");
    const box = createUi();

    (siteSub || heading).parentNode.insertBefore(
      box,
      siteSub ? siteSub.nextSibling : heading.nextSibling
    );

    document.getElementById("generate-btn").onclick = generateSummary;

    const saveBtn = document.getElementById("save-key-btn");
    const editBtn = document.getElementById("edit-key-btn");
    const keySection = document.getElementById("key-section");
    const keyInput = document.getElementById("api-key-input");

    if (saveBtn) {
      saveBtn.onclick = () => {
  if (!keyInput.value.trim()) return;
  saveApiKey(keyInput.value);
  keySection.style.display = "none";
  editBtn.style.display = "block";
  document.getElementById("generate-btn").disabled = false;
};
    }

    if (editBtn) {
      editBtn.onclick = () => {
  keyInput.value = getApiKey();
  keySection.style.display = "block";
  editBtn.style.display = "none";
  document.getElementById("generate-btn").disabled = true;
};
    }
  }

  init();
})();

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.