blackfalcon1210 / Amazon Books AI Review Detector (Deep Scan + Titles)

// ==UserScript==
// @name         Amazon Books AI Review Detector (Deep Scan + Titles)
// @namespace    blackfalcon1210
// @version      2.3
// @description  Scans 1 & 2-star review bodies and titles on Amazon for AI red flags and reports the percentage of flagged reviews.
// @author       Blackfalcon1210
// @license      MIT
// @include      /^https?:\/\/(www|smile)\.amazon\.(cn|in|co\.jp|sg|se|ae|fr|de|pl|it|nl|es|co\.uk|ca|com(\.(mx|au|br|tr|be))?)\/.*(dp|gp\/(product|video)|exec\/obidos\/ASIN|o\/ASIN|product-reviews)\/.*$/
// @grant        GM_xmlhttpRequest
// @connect      amazon.com
// @connect      amazon.co.uk
// @connect      amazon.ca
// @connect      amazon.de
// @connect      amazon.fr
// @connect      amazon.it
// @connect      amazon.es
// @connect      amazon.co.jp
// @connect      amazon.in
// @noframes
// @run-at       document-end
// ==/UserScript==

/* jshint esversion: 6 */
(function(doc) {
    'use strict';

    // Edit the terms you want this script to look for in reviews.
    const redFlags = [
        "ai",
        "ai slop",
        "ai written",
        "written with ai",
        "programmed ai",
        "much ai",
        "ai writer",
        "ai generated",
        "its ai",
        "it's ai",
        "ai trash",
        "ai writing",
        "work of ai"
    ];

    const maxPagesToScan = 10;

    let statusWidget = null;
    let scanInitiated = false;

    let totalReviewsScanned = 0;
    let flaggedReviews = 0;

    // Prevent the same review from being counted twice.
    const scannedReviewIds = new Set();

    function getLeftColumnTarget() {
        return doc.getElementById('litb-canvas-click-wrapper') ||
               doc.getElementById('imgBlkFront') ||
               doc.getElementById('imageBlock') ||
               doc.getElementById('booksImageBlock_feature_div');
    }

    function createStatusWidget() {
        if (statusWidget) return;

        const targetAnchor = getLeftColumnTarget();
        if (!targetAnchor) return;

        statusWidget = doc.createElement('div');
        statusWidget.id = 'amazon-ai-detector-status-card';

        Object.assign(statusWidget.style, {
            width: '100%',
            backgroundColor: '#FEF3C7',
            border: '2px solid #F59E0B',
            borderRadius: '8px',
            padding: '10px 14px',
            margin: '10px 0 15px 0',
            fontFamily: '"Amazon Ember", Arial, sans-serif',
            boxSizing: 'border-box',
            fontSize: '13px',
            fontWeight: 'bold',
            color: '#92400E',
            textAlign: 'center',
            display: 'block',
            clear: 'both',
            lineHeight: '1.5'
        });

        statusWidget.textContent =
            "🔍 Scanning 1 & 2-star reviews for AI terms...";

        targetAnchor.parentNode.insertBefore(
            statusWidget,
            targetAnchor.nextSibling
        );
    }

    function updateScanningStatus(page) {
        if (!statusWidget) return;

        statusWidget.textContent =
            `🔍 Scanning 1 & 2-star reviews... ` +
            `Page ${page} of ${maxPagesToScan} | ` +
            `${totalReviewsScanned} reviews checked`;
    }

    function showFinalStatus(scanIncomplete = false) {
        if (!statusWidget) return;

        const percentage = totalReviewsScanned > 0
            ? (flaggedReviews / totalReviewsScanned) * 100
            : 0;

        /*
         * Show one decimal place, except when there are no flags.
         * Examples: 0%, 2.5%, 10.0%
         */
        const percentageText = flaggedReviews === 0
            ? "0%"
            : `${percentage.toFixed(1)}%`;

        if (flaggedReviews > 0) {
            statusWidget.style.backgroundColor = '#FEF2F2';
            statusWidget.style.border = '2px solid #F87171';
            statusWidget.style.color = '#991B1B';

            statusWidget.innerHTML =
                `⚠️ AI Usage Mentioned in Reviews<br>` +
                `${flaggedReviews} of ${totalReviewsScanned} scanned reviews ` +
                `were flagged (${percentageText})`;
        } else {
            statusWidget.style.backgroundColor = '#F0FDF4';
            statusWidget.style.border = '2px solid #4ADE80';
            statusWidget.style.color = '#166534';

            statusWidget.innerHTML =
                `✅ No AI Flags Found<br>` +
                `0 of ${totalReviewsScanned} scanned reviews ` +
                `were flagged (${percentageText})`;
        }

        if (scanIncomplete) {
            statusWidget.innerHTML +=
                `<br><span style="font-size:11px;font-weight:normal;">` +
                `Scan ended early because Amazon did not return another review page.` +
                `</span>`;
        }
    }

    function extractAsin() {
        const asinInput =
            doc.getElementById('ASIN') ||
            doc.querySelector('[data-asin]');

        if (asinInput) {
            const asin =
                asinInput.value ||
                asinInput.getAttribute('data-asin');

            if (asin && /^[A-Z0-9]{10}$/i.test(asin)) {
                return asin;
            }
        }

        const match = window.location.href.match(
            /\/(?:dp|gp\/product|exec\/obidos\/ASIN|o\/ASIN)\/([A-Z0-9]{10})/i
        );

        // Return only the captured ASIN, not the entire regex result.
        return match ? match[1] : null;
    }

    function checkTextForFlags(text) {
        const cleanText = text
            .toLowerCase()
            .replace(/\s+/g, ' ')
            .trim();

        return redFlags.some(flag => {
            const cleanFlag = flag.toLowerCase();

            /*
             * "ai" by itself needs word boundaries. Otherwise words such as
             * "said", "paid", "Gail", and "detail" could create false flags.
             */
            if (cleanFlag === "ai") {
                return /\bai\b/i.test(cleanText);
            }

            return cleanText.includes(cleanFlag);
        });
    }

    function getReviewIdentifier(review, page, index) {
        return review.getAttribute('id') ||
               review.getAttribute('data-review-id') ||
               `${page}-${index}-${(review.textContent || "").slice(0, 100)}`;
    }

    function scanAllReviewPages(asin, page = 1) {
        updateScanningStatus(page);

        const domain = window.location.hostname;
        const reviewsUrl =
            `https://${domain}/product-reviews/${asin}/` +
            `?pageNumber=${page}&sortBy=recent&filterByStar=critical`;

        GM_xmlhttpRequest({
            method: "GET",
            url: reviewsUrl,

            onload: function(response) {
                if (response.status !== 200) {
                    showFinalStatus(true);
                    return;
                }

                const parser = new DOMParser();
                const htmlDoc = parser.parseFromString(
                    response.responseText,
                    "text/html"
                );

                // Includes both the review title and review body.
                const reviews = htmlDoc.querySelectorAll(
                    '[data-hook="review"]'
                );

                if (reviews.length === 0) {
                    showFinalStatus(page > 1);
                    return;
                }

                reviews.forEach((review, index) => {
                    const reviewId = getReviewIdentifier(
                        review,
                        page,
                        index
                    );

                    if (scannedReviewIds.has(reviewId)) return;

                    scannedReviewIds.add(reviewId);
                    totalReviewsScanned++;

                    if (checkTextForFlags(review.textContent || "")) {
                        flaggedReviews++;
                    }
                });

                if (page < maxPagesToScan) {
                    updateScanningStatus(page);

                    setTimeout(() => {
                        scanAllReviewPages(asin, page + 1);
                    }, 200);
                } else {
                    showFinalStatus(false);
                }
            },

            onerror: function() {
                showFinalStatus(true);
            }
        });
    }

    function initScan() {
        if (scanInitiated) return;

        const asin = extractAsin();
        if (!asin) return;

        scanInitiated = true;
        createStatusWidget();
        scanAllReviewPages(asin, 1);
    }

    const layoutObserver = new MutationObserver(() => {
        if (getLeftColumnTarget() && !scanInitiated) {
            initScan();
            layoutObserver.disconnect();
        }
    });

    layoutObserver.observe(doc.documentElement, {
        childList: true,
        subtree: true
    });

    if (getLeftColumnTarget()) {
        initScan();
    }
})(document);