OriginsHelper

Instantly toggle a grouped macro-region DNA ethnicity table with confidence ranges on Ancestry

// ==UserScript==
// @name         OriginsHelper
// @namespace    https://greasyfork.org/en/users/1525357-strike978
// @version      0.3
// @description  Instantly toggle a grouped macro-region DNA ethnicity table with confidence ranges on Ancestry
// @author       Omar Nunez
// @include      /^https:\/\/www\.ancestry\.[a-z.]+\/dna\/origins\/.*/
// @include      /^https:\/\/www\.ancestry\.[a-z.]+\/discoveryui-matches\/compare\/.*\/with\/.*/
// @license      CC BY-NC 4.0
// @grant        none
// @run-at       document-end
// ==/UserScript==

/*
 * OriginsHelper - AncestryDNA Ethnicity Table Enhancer
 * Copyright (c) 2025 Omar Nunez
 * 
 * Licensed under Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
 * https://creativecommons.org/licenses/by-nc/4.0/
 */


(function() {
    'use strict';

    // Constants
    const BASE_ORIGIN = window.location.origin;
    const ETHNICITY_API_URL = (testId) => `${BASE_ORIGIN}/dna/origins/secure/tests/${testId}/v2/ethnicity`;
    const NAMES_API_URL = `${BASE_ORIGIN}/dna/origins/public/ethnicity/2025/names`;
    const HTML2CANVAS_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js';
    // Added: Useful external links as constants
    const CONFIDENCE_RANGE_HELP_URL = `${BASE_ORIGIN}/cs/dna-help/ethnicity/bootstrapping`;
    const REGION_DETAILS_BASE_URL = `${BASE_ORIGIN}/dna/origins`;

    // Batch Ethnicity API (for compare pages)
    const BATCH_ETHNICITY_API_URL = (baseId) => `${BASE_ORIGIN}/dna/origins/secure/compare/${baseId}/batchEthnicity`;
    
    // Utility: Extract compare IDs from URL
    function extractCompareIds() {
        // URL pattern: /discoveryui-matches/compare/{id1}/with/{id2}(/ethnicity)?
        const match = window.location.pathname.match(/\/compare\/([A-Fa-f0-9-]+)\/with\/([A-Fa-f0-9-]+)/i);
        if (!match) return null;
        return { id1: match[1], id2: match[2] };
    }

    // Insert buttons directly on compare page (used by observer)
    function insertComparePageButtons(compareDataEl) {
        const ids = extractCompareIds();
        if (!ids) {
            console.log('OriginsHelper: No compare IDs found in URL');
            return;
        }
        
        // Double-check that we're not inserting duplicates
        if (document.querySelector('button[data-origins-helper="ranges"]')) {
            console.log('OriginsHelper: Buttons already exist, skipping insertion');
            return;
        }
        
        console.log('OriginsHelper: Creating buttons for compare page');
        
        // Create button wrapper
        const buttonWrapper = document.createElement('div');
        buttonWrapper.style.cssText = 'margin-bottom:12px; display:flex; align-items:center; gap:10px;';
        buttonWrapper.setAttribute('data-origins-helper', 'wrapper'); // Add tracking
        
        // Create ranges button
        const rangesBtn = document.createElement('button');
        rangesBtn.setAttribute('data-origins-helper', 'ranges'); // Add tracking attribute
        rangesBtn.innerHTML = ICONS.ranges + 'Show Confidence Ranges';
        rangesBtn.style.cssText = 'padding:7px 16px; font-size:15px; font-weight:600; background:#2563eb; color:#fff; border:1.5px solid #1e40af; border-radius:6px; cursor:pointer; display:flex; align-items:center; transition:background 0.15s;';
        rangesBtn.onmouseover = () => { rangesBtn.style.background = '#1d4ed8'; };
        rangesBtn.onmouseout = () => { rangesBtn.style.background = rangesBtn.disabled ? '#6b7280' : '#2563eb'; };

        rangesBtn.onclick = async () => {
            try {
                rangesBtn.innerHTML = ICONS.loading + 'Loading...';
                rangesBtn.disabled = true;
                rangesBtn.style.background = '#6b7280';
                
                // Use batch API to get confidence ranges for both people at once
                const batchUrl = BATCH_ETHNICITY_API_URL(ids.id1.toUpperCase());
                const payload = [ids.id1.toUpperCase(), ids.id2.toUpperCase()];
                
                const response = await fetch(batchUrl, {
                    method: 'PUT',
                    headers: { 'Content-Type': 'application/json' },
                    credentials: 'include',
                    body: JSON.stringify(payload)
                });
                
                const batchData = await response.json();
                
                // Add confidence ranges to the page
                await addConfidenceRangesToPageBatch(batchData, ids);
                
                // Update button to success state
                rangesBtn.innerHTML = ICONS.check + 'Ranges Visible';
                rangesBtn.style.background = '#16a34a';
                rangesBtn.onmouseout = () => { rangesBtn.style.background = '#16a34a'; };
            } catch (err) {
                console.error('Error adding confidence ranges:', err);
                rangesBtn.innerHTML = ICONS.ranges + 'Failed - Retry';
                rangesBtn.disabled = false;
                rangesBtn.style.background = '#dc2626';
                rangesBtn.onmouseout = () => { rangesBtn.style.background = '#dc2626'; };
            }
        };
        
        // Create screenshot button
        const screenshotBtn = createScreenshotButton(() => {
            // Find the grid element but exclude the buttons by targeting just the data
            const compareData = document.querySelector('.compare-data');
            if (compareData) takeScreenshot(compareData);
        });
        screenshotBtn.style.marginLeft = '0'; // Remove default margin since we're using gap
        
        // Add buttons to wrapper
        buttonWrapper.appendChild(rangesBtn);
        buttonWrapper.appendChild(screenshotBtn);
        
        // Simple insertion - find the best location and insert
        const insertTarget = document.querySelector('.compare-data') || 
                             document.querySelector('[role="grid"]') || 
                             compareDataEl;
        
        if (insertTarget && insertTarget.parentNode) {
            insertTarget.parentNode.insertBefore(buttonWrapper, insertTarget);
        }
    }




    // Add confidence ranges to the compare page using batch API data
    async function addConfidenceRangesToPageBatch(batchData, ids) {
        try {
            const person1Id = ids.id1.toUpperCase();
            const person2Id = ids.id2.toUpperCase();
            
            // Collect all region keys from both people to fetch names
            const allRegionKeys = new Set();
            
            // Collect region keys from person 1
            if (batchData[person1Id] && batchData[person1Id].regions) {
                batchData[person1Id].regions.forEach(region => {
                    allRegionKeys.add(region.key);
                });
            }
            
            // Collect region keys from person 2  
            if (batchData[person2Id] && batchData[person2Id].regions) {
                batchData[person2Id].regions.forEach(region => {
                    allRegionKeys.add(region.key);
                });
            }
            
            // Fetch region names from the Names API
            const namesMap = await fetchRegionNames(Array.from(allRegionKeys));
            
            // Create maps of region name to confidence data for both people
            const person1RangesByName = {};
            const person2RangesByName = {};
            
            // Process person 1 data from batch response
            if (batchData[person1Id] && batchData[person1Id].regions) {
                batchData[person1Id].regions.forEach(region => {
                    // Use the fetched region name as the key
                    const regionName = namesMap[region.key] || region.key;
                    person1RangesByName[regionName] = {
                        lower: Math.round(region.lowerConfidence || 0),
                        upper: Math.round(region.upperConfidence || 0),
                        percentage: Math.round(region.percentage || 0)
                    };
                });
            }
            
            // Process person 2 data from batch response
            if (batchData[person2Id] && batchData[person2Id].regions) {
                batchData[person2Id].regions.forEach(region => {
                    // Use the fetched region name as the key
                    const regionName = namesMap[region.key] || region.key;
                    person2RangesByName[regionName] = {
                        lower: Math.round(region.lowerConfidence || 0),
                        upper: Math.round(region.upperConfidence || 0),
                        percentage: Math.round(region.percentage || 0)
                    };
                });
            }
            // Find all rows with percentage data
            const rows = document.querySelectorAll('[role="row"]');
            
            rows.forEach((row, rowIndex) => {
                // Look for row header (region name) and gridcells (percentages)
                const rowHeader = row.querySelector('[role="rowheader"]');
                const gridCells = row.querySelectorAll('[role="gridcell"]');
                
                if (!rowHeader || gridCells.length < 2) return;
                
                // Skip the duplicate/hidden rows (they have hideVisually class in rowheader)
                if (rowHeader.classList.contains('hideVisually')) {
                    return;
                }
                
                // Extract region name from row header
                let regionName = rowHeader.textContent?.trim();
                if (!regionName) return;
                
                // Clean up region name - remove macro region prefix if present
                // Example: "Iberian Peninsula : Portugal" -> "Portugal"
                if (regionName.includes(' : ')) {
                    regionName = regionName.split(' : ')[1].trim();
                }
                
                // Process each gridcell in this row
                gridCells.forEach((cell, cellIndex) => {
                    const text = cell.textContent?.trim();
                    
                    // Skip cells that don't contain just a percentage (already have ranges or are not percentages)
                    if (!text || !/^\d+%$/.test(text)) {
                        return;
                    }
                    
                    const percentage = parseInt(text.replace('%', ''));
                    
                    // Skip cells with 0% - no need to add ranges for regions someone doesn't have
                    if (percentage === 0) {
                        return;
                    }
                    
                    // Determine which person based on cell position (0 = left/person1, 1 = right/person2)
                    const isLeftPerson = cellIndex === 0;
                    const ranges = isLeftPerson ? person1RangesByName : person2RangesByName;
                    
                    // Look for matching region in the ranges
                    let matchedRange = null;
                    
                    // Debug logging
                    console.log(`Looking for match: regionName="${regionName}", percentage=${percentage}, isLeftPerson=${isLeftPerson}`);
                    
                    // First try exact name match with percentage verification
                    if (ranges[regionName] && ranges[regionName].percentage === percentage) {
                        matchedRange = ranges[regionName];
                        console.log(`✅ Exact match found: ${regionName} = ${percentage}% (${matchedRange.lower}-${matchedRange.upper}%)`);
                    } else {
                        // Try percentage match first (most reliable) - but ensure it's a reasonable match
                        for (const [rangeName, range] of Object.entries(ranges)) {
                            if (range.percentage === percentage) {
                                // Additional check: make sure the region names are somewhat related
                                const regionLower = regionName.toLowerCase();
                                const rangeLower = rangeName.toLowerCase();
                                
                                // If percentage matches exactly, use it (unless names are completely unrelated)
                                if (rangeLower.includes(regionLower) || regionLower.includes(rangeLower) || 
                                    rangeLower === regionLower || percentage >= 5) { // Accept high percentages even with name mismatches
                                    matchedRange = range;
                                    console.log(`✅ Percentage match found: ${regionName} (${percentage}%) matched with ${rangeName} (${range.percentage}%) = (${range.lower}-${range.upper}%)`);
                                    break;
                                }
                            }
                        }
                        
                        // If still no match, try fuzzy name matching with percentage proximity
                        if (!matchedRange) {
                            let bestMatch = null;
                            let bestScore = 0;
                            
                            for (const [rangeName, range] of Object.entries(ranges)) {
                                const regionLower = regionName.toLowerCase();
                                const rangeLower = rangeName.toLowerCase();
                                
                                // Calculate matching score
                                let score = 0;
                                if (rangeLower.includes(regionLower) || regionLower.includes(rangeLower)) {
                                    score += 10; // Name similarity bonus
                                }
                                
                                // Percentage proximity bonus (closer percentages get higher scores)
                                const percentageDiff = Math.abs(range.percentage - percentage);
                                if (percentageDiff <= 2) score += 5;
                                else if (percentageDiff <= 5) score += 2;
                                
                                if (score > bestScore && score >= 5) { // Minimum threshold
                                    bestScore = score;
                                    bestMatch = range;
                                }
                            }
                            
                            if (bestMatch) {
                                matchedRange = bestMatch;
                                console.log(`✅ Fuzzy match found: ${regionName} (${percentage}%) matched with score ${bestScore} = (${matchedRange.lower}-${matchedRange.upper}%)`);
                            }
                        }
                    }
                    
                    if (!matchedRange) {
                        console.log(`❌ No match found for: ${regionName} (${percentage}%)`);
                    }
                    
                    if (matchedRange) {
                        // Add confidence range to cell
                        const rangeText = ` (${matchedRange.lower}-${matchedRange.upper}%)`;
                        cell.textContent = cell.textContent + rangeText;
                    }
                });
            });
            
        } catch (err) {
            console.error('Error adding confidence ranges:', err);
        }
    }
    
    // SVG Icons
    const ICONS = {
        table: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:7px;"><rect x="3" y="6" width="18" height="12" rx="2.5" stroke="#fff" stroke-width="2" fill="none"/><path d="M3 14h18" stroke="#fff" stroke-width="2"/></svg>',
        eyeOff: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:7px;"><path d="M17.94 17.94A10.06 10.06 0 0 1 12 19c-5 0-9.27-3.11-11-7 1.13-2.47 3.13-4.5 5.66-5.66m3.1-1.01A9.77 9.77 0 0 1 12 5c5 0 9.27 3.11 11 7a11.18 11.18 0 0 1-2.06 3.11M1 1l22 22" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke="#fff" stroke-width="2" fill="none"/></svg>',
        camera: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:6px;"><rect x="3" y="7" width="18" height="12" rx="2" stroke="#fff" stroke-width="2" fill="#22c55e"/><circle cx="12" cy="13" r="3.5" stroke="#fff" stroke-width="2" fill="#22c55e"/><rect x="7" y="4" width="2" height="3" rx="1" fill="#22c55e" stroke="#fff" stroke-width="2"/></svg>',
        info: '<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" style="display:inline; vertical-align:middle;"><circle cx="12" cy="12" r="10" stroke="#2563eb" stroke-width="2" fill="#fff"/><path d="M12 8.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-1 3.5h2v5h-2v-5z" fill="#2563eb"/></svg>',
        ranges: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:6px;"><path d="M3 12h18m-9-9v18" stroke="#fff" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="12" r="2" fill="#fff"/></svg>',
        check: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:6px;"><path d="M20 6L9 17l-5-5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
        loading: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style="vertical-align:middle; margin-right:6px; animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke="#fff" stroke-width="2" stroke-dasharray="31.416" stroke-dashoffset="31.416" opacity="0.3"/><path d="M12 2a10 10 0 0 1 10 10" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg><style>@keyframes spin { to { transform: rotate(360deg); } }</style>'
    };

    // Utility Functions
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            
            const observer = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) {
                    observer.disconnect();
                    resolve(el);
                }
            });
            
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => {
                observer.disconnect();
                reject(new Error('Timeout waiting for ' + selector));
            }, timeout);
        });
    }

    function extractTestId() {
        const pathMatch = window.location.pathname.match(/\/dna\/origins\/([A-Fa-f0-9-]+)/i);
        if (!pathMatch) throw new Error('Could not extract test ID from URL: ' + window.location.pathname);
        return pathMatch[1];
    }

    // API Functions
    async function fetchEthnicityData(testId) {
        const response = await fetch(ETHNICITY_API_URL(testId), { credentials: 'include' });
        if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        return response.json();
    }

    async function fetchRegionNames(keys) {
        const response = await fetch(NAMES_API_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(keys)
        });
        if (!response.ok) throw new Error(`Names API HTTP ${response.status}: ${response.statusText}`);
        return response.json();
    }

    // Origins Table Functions (for individual ethnicity pages only)  
    function groupRegionsByMacro(regions) {
        const macroGroups = {};
        for (const region of regions) {
            if (!macroGroups[region.macroRegionKey]) macroGroups[region.macroRegionKey] = [];
            macroGroups[region.macroRegionKey].push(region);
        }
        return macroGroups;
    }

    function calculateMacroTotals(macroGroups) {
        return Object.keys(macroGroups)
            .map(macroKey => {
                const regions = macroGroups[macroKey];
                const macroTotal = regions.reduce((sum, r) => sum + (r.percentage || 0), 0);
                return { macroKey, regions, macroTotal };
            })
            .sort((a, b) => b.macroTotal - a.macroTotal);
    }

    function sortRegions(regions) {
        return [...regions].sort((a, b) => {
            if (b.percentage !== a.percentage) return b.percentage - a.percentage;
            return b.lowerConfidence - a.lowerConfidence;
        });
    }

    async function generateSummaryTable() {
        try {
            const testId = extractTestId();
            const data = await fetchEthnicityData(testId);
            
            const regions = Array.isArray(data.regions) ? data.regions : [];
            const regionKeys = Array.from(new Set(regions.map(r => r.key)));
            const macroRegionKeys = Array.from(new Set(regions.map(r => r.macroRegionKey)));
            const allKeys = [...regionKeys, ...macroRegionKeys];
            
            const namesMap = await fetchRegionNames(allKeys);
            const macroGroups = groupRegionsByMacro(regions);
            const macroTotals = calculateMacroTotals(macroGroups);
            
            // Build table HTML
            let html = `<table style=\"border-collapse:collapse; font-size:13px; background:#fff; margin-top:6px; width:auto;\">
                <thead><tr>
                    <th style=\"border:1px solid #ccc; padding:3px 8px;\">Macro-Region</th>
                    <th style=\"border:1px solid #ccc; padding:3px 8px;\">Total %</th>
                    <th style=\"border:1px solid #ccc; padding:3px 8px;\">Region</th>
                    <th style=\"border:1px solid #ccc; padding:3px 8px;\">%<\/th>
                    <th style=\"border:1px solid #ccc; padding:3px 8px;\"><a href=\"${CONFIDENCE_RANGE_HELP_URL}\" target=\"_blank\" style=\"color:#1a0dab; text-decoration:none; display:inline;\" title=\"About confidence ranges\">${ICONS.info}<\/a> Range<\/th>
                <\/tr><\/thead><tbody>`;
                
            for (const { macroKey, regions, macroTotal } of macroTotals) {
                const macroName = namesMap[macroKey] || macroKey;
                const sortedRegions = sortRegions(regions);
                
                let firstRow = true;
                for (const region of sortedRegions) {
                    const regionName = namesMap[region.key] || region.key;
                    const regionUrl = `${REGION_DETAILS_BASE_URL}/${testId}/regions/${region.key}`;
                    
                    html += '<tr>';
                    if (firstRow) {
                        html += `<td style="border:1px solid #ccc; padding:3px 8px; font-weight:bold; background:#f7f7e6;" rowspan="${regions.length}">${macroName}</td>`;
                        html += `<td style="border:1px solid #ccc; padding:3px 8px; font-weight:bold; background:#f7f7e6; text-align:right;" rowspan="${regions.length}">${Math.round(macroTotal)}%</td>`;
                        firstRow = false;
                    }
                    html += `<td style="border:1px solid #ccc; padding:3px 8px;"><a href="${regionUrl}" target="_blank" style="color:#1a0dab;"><strong>${regionName}</strong></a></td>`;
                    html += `<td style="border:1px solid #ccc; padding:3px 8px; text-align:right;">${Math.round(region.percentage)}%</td>`;
                    html += `<td style="border:1px solid #ccc; padding:3px 8px; text-align:right;">${Math.round(region.lowerConfidence)}–${Math.round(region.upperConfidence)}%</td>`;
                    html += '</tr>';
                }
            }
            html += '</tbody></table>';
            return html;
        } catch (err) {
            return `<strong>OriginsHelper:</strong> Error: ${err.message}`;
        }
    }

    // Screenshot Functions
    function createScreenshotButton(onClick) {
        const btn = document.createElement('button');
        btn.innerHTML = ICONS.camera + 'Take Screenshot';
        btn.title = 'Take Screenshot';
        btn.style.cssText = 'padding:7px 16px; font-size:15px; font-weight:600; background:#22c55e; color:#fff; border:1.5px solid #15803d; border-radius:6px; cursor:pointer; display:flex; align-items:center; transition:background 0.15s;';
        btn.onmouseover = () => { btn.style.background = '#16a34a'; };
        btn.onmouseout = () => { btn.style.background = '#22c55e'; };
        btn.addEventListener('click', onClick);
        return btn;
    }

    async function loadHtml2Canvas() {
        if (!window.html2canvas) {
            const script = document.createElement('script');
            script.src = HTML2CANVAS_CDN;
            document.head.appendChild(script);
            await new Promise(r => { script.onload = r; });
        }
    }

    async function takeScreenshot(targetEl) {
        await loadHtml2Canvas();
        window.html2canvas(targetEl, { backgroundColor: '#fff' }).then(canvas => {
            const dataUrl = canvas.toDataURL('image/png');
            const win = window.open();
            win.document.write('<title>Screenshot</title><img src="' + dataUrl + '" style="max-width:100%;">');
        });
    }



    // Origins Table Toggle (for individual ethnicity pages)
    class OriginsHelper {
        constructor() {
            this.showingTable = false;
            this.tableDiv = null;
            this.screenshotBtn = null;
            this.macroHTML = '';
        }

        async init() {
            try {
                const macroEl = await waitForElement('ul.macroRegions');
                this.macroHTML = macroEl.outerHTML;
                this.createUI(macroEl);
            } catch (err) {
                console.error('OriginsHelper: Failed to initialize', err);
            }
        }

        createUI(macroEl) {
            const toggleWrapper = document.createElement('div');
            toggleWrapper.style.cssText = 'margin-bottom:14px; display:flex; align-items:center; gap:18px;';
            
            const toggleBtn = document.createElement('button');
            toggleBtn.innerHTML = ICONS.table + '<span style="margin-left:7px;">View Grouped Table</span>';
            toggleBtn.style.cssText = 'padding:7px 18px; font-size:17px; font-weight:600; color:#fff; background:#2563eb; border:1.5px solid #1e40af; border-radius:7px; cursor:pointer; display:flex; align-items:center;';
            toggleBtn.onmouseover = () => { toggleBtn.style.background = '#1d4ed8'; };
            toggleBtn.onmouseout = () => { toggleBtn.style.background = this.showingTable ? '#dc2626' : '#2563eb'; };
            
            toggleWrapper.appendChild(toggleBtn);
            macroEl.parentNode.insertBefore(toggleWrapper, macroEl);
            
            toggleBtn.addEventListener('click', () => this.handleToggle(toggleBtn, toggleWrapper));
            this.toggleWrapper = toggleWrapper;
        }

        async handleToggle(toggleBtn, toggleWrapper) {
            if (!this.showingTable) {
                await this.showTable(toggleBtn, toggleWrapper);
            } else {
                this.hideTable(toggleBtn, toggleWrapper);
            }
        }

        async showTable(toggleBtn, toggleWrapper) {
            const currentMacro = document.querySelector('ul.macroRegions');
            if (!currentMacro) return;
            
            this.tableDiv = document.createElement('div');
            this.tableDiv.innerHTML = 'Loading...';
            currentMacro.parentNode.replaceChild(this.tableDiv, currentMacro);
            
            const html = await generateSummaryTable();
            this.tableDiv.innerHTML = html;
            
            this.showingTable = true;
            toggleBtn.innerHTML = ICONS.eyeOff + '<span style="margin-left:7px;">Back to Default View</span>';
            toggleBtn.style.background = '#dc2626';
            
            if (!this.screenshotBtn) {
                this.screenshotBtn = createScreenshotButton(() => {
                    const table = this.tableDiv.querySelector('table');
                    if (table) takeScreenshot(table);
                });
                toggleWrapper.appendChild(this.screenshotBtn);
            }
        }

        hideTable(toggleBtn, toggleWrapper) {
            const macroList = document.createElement('div');
            macroList.innerHTML = this.macroHTML;
            const newMacro = macroList.firstElementChild;
            this.tableDiv.parentNode.replaceChild(newMacro, this.tableDiv);
            
            this.showingTable = false;
            toggleBtn.innerHTML = ICONS.table + '<span style="margin-left:7px;">View Grouped Table</span>';
            toggleBtn.style.background = '#2563eb';
            
            if (this.screenshotBtn) {
                toggleWrapper.removeChild(this.screenshotBtn);
                this.screenshotBtn = null;
            }
        }
    }


    // Initialize immediately for origins pages
    if (!window.location.pathname.includes('/discoveryui-matches/compare/')) {
        const app = new OriginsHelper();
        app.init();
    }
    
    // SMART APPROACH: Trigger on base compare URL, wait for ethnicity tab
    if (window.location.pathname.includes('/discoveryui-matches/compare/') && window.location.pathname.includes('/with/')) {
        console.log('✨ OriginsHelper: Compare page detected - waiting for ethnicity tab');
        
        let attemptCount = 0;
        const maxAttempts = 60; // Try for 1 minute
        
        function waitForEthnicityAndInsert() {
            attemptCount++;
            console.log(`⏳ Attempt ${attemptCount}: Checking for ethnicity tab...`);
            
            // Check if buttons already exist
            if (document.querySelector('button[data-origins-helper="ranges"]')) {
                console.log('✅ Buttons already exist, stopping');
                return true;
            }
            
            // Look for ethnicity tab link or ethnicity content
            const ethnicityLinks = document.querySelectorAll('a[href*="/ethnicity"]');
            const ethnicityContent = document.querySelector('[role="grid"], .compare-data, [role="row"]');
            const percentageElements = document.querySelectorAll('*');
            let hasEthnicityData = false;
            
            // Check if we have percentage data that looks like ethnicity
            for (const el of percentageElements) {
                const text = el.textContent || '';
                if (text.includes('%') && (text.includes('Iberian') || text.includes('Germanic') || text.includes('Celtic') || text.includes('Scandinavian') || text.includes('European'))) {
                    hasEthnicityData = true;
                    break;
                }
            }
            
            if (ethnicityLinks.length > 0) {
                console.log('📍 Found ethnicity tab link, clicking it...');
                ethnicityLinks[0].click();
                
                // Wait a moment for content to load, then try to insert
                setTimeout(() => {
                    const target = document.querySelector('[role="grid"], .compare-data') || document.body.firstElementChild;
                    if (target) {
                        console.log('🚀 Inserting buttons after clicking ethnicity tab');
                        insertComparePageButtons(target);
                    }
                }, 1500);
                return true;
            } else if (hasEthnicityData || ethnicityContent) {
                console.log('📊 Found ethnicity data, inserting buttons');
                const target = ethnicityContent || document.body.firstElementChild;
                insertComparePageButtons(target);
                return true;
            }
            
            console.log(`⏳ No ethnicity content yet (attempt ${attemptCount}/${maxAttempts})`);
            return false;
        }
        
        // Start checking immediately
        setTimeout(() => {
            console.log('✨ Starting ethnicity detection...');
            
            const interval = setInterval(() => {
                if (waitForEthnicityAndInsert() || attemptCount >= maxAttempts) {
                    clearInterval(interval);
                    if (attemptCount >= maxAttempts) {
                        console.log('❌ Gave up waiting for ethnicity content');
                    }
                }
            }, 2000); // Check every 2 seconds
            
        }, 1000); // Wait 1 second for initial page load
        
        // ALSO: Watch for URL changes (when switching between tabs)
        let lastUrl = window.location.href;
        const urlWatcher = setInterval(() => {
            if (window.location.href !== lastUrl) {
                lastUrl = window.location.href;
                console.log('🔄 URL changed, checking for ethnicity content again...');
                
                // Reset attempt count and check again
                setTimeout(() => {
                    if (window.location.href.includes('/ethnicity')) {
                        console.log('🎯 Now on ethnicity tab, inserting buttons...');
                        attemptCount = 0; // Reset counter
                        waitForEthnicityAndInsert();
                    }
                }, 500);
            }
        }, 1000);
        
        // ALSO: Watch for content changes (mutation observer)
        const contentObserver = new MutationObserver(() => {
            // Only check if we're on ethnicity tab and don't have buttons yet
            if (window.location.href.includes('/ethnicity') && !document.querySelector('button[data-origins-helper="ranges"]')) {
                console.log('🔄 Content changed on ethnicity tab, checking for insertion...');
                setTimeout(() => {
                    waitForEthnicityAndInsert();
                }, 1000);
            }
        });
        
        contentObserver.observe(document.body, { 
            childList: true, 
            subtree: true 
        });
    }

})();