AI Detector

Highlight likely AI-generated text using configurable heuristics

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         AI Detector
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Highlight likely AI-generated text using configurable heuristics
// @author       anrinion
// @license      MIT
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ==================== CONFIGURATION & STATE ====================
    const DEFAULT_CONFIG = {
        // Global
        threshold: 6,
        debounceMs: 10000,
        excludedDomains: [],
        debug: false,

        heuristics: {
            punctuationMonotony: {
                enabled: true,
                weight: 6,
                emDashPer200Words: 3,
                curlyQuotesBonus: 0.2
            },
            homoglyph: {
                enabled: true,
                weight: 10
            },
            multilingualLeakage: {
                enabled: true,
                weight: 9,
                cjkRange: true,
                persianRange: true,
                leakedStrings: ['破', '撒']
            },
            vocabulary: {
                enabled: true,
                weight: 7,
                highRiskWords: [
                    'delve', 'tapestry', 'testament', 'realm', 'pivotal', 'vibrant',
                    'unleash', 'unlock', 'robust', 'seamless', 'operational excellence',
                    'strategic alignment', 'in today\'s digital age', 'brilliant'
                ],
                thresholdPer250Words: 3
            },
            emojiSignature: {
                enabled: true,
                weight: 5,
                aiEmojis: ['✨', '🚀']
            },
            formattingArtifacts: {
                enabled: true,
                weight: 7,
                latexDelimiterPatterns: ['\\[', '\\]', '\\(', '\\)'],
                titleCaseThreshold: 0.6
            },
            structuralUniformity: {
                enabled: true,
                weight: 5,
                uniformSentenceLengthRange: [15, 25],
                uniformityThreshold: 0.9
            },
            cannedDisclaimers: {
                enabled: true,
                weight: 10,
                phrases: [
                    'as an ai language model',
                    'as of my last knowledge update',
                    'it is important to note that',
                    'i hope this finds you well'
                ]
            },
            lackOfSpecificity: {
                enabled: true,
                weight: 4,
                vagueTerms: [
                    'a recent study', 'many people', 'experts say', 'research shows',
                    'it is widely believed', 'some argue'
                ],
                thresholdRatio: 2.0
            }
        }
    };

    let config = loadConfig();
    let scanTimer = null;
    let observer = null;
    let tooltipEl = null;
    let settingsPanel = null;
    let isApplyingHighlights = false;   // Prevent recursive scans

    // Debug logging
    function log(...args) {
        if (config.debug) console.log('[AI-Highlighter]', ...args);
    }

    function warn(...args) {
        if (config.debug) console.warn('[AI-Highlighter]', ...args);
    }

    // ==================== PERSISTENCE ====================
    function loadConfig() {
        const saved = GM_getValue('aiHighlighterConfig', null);
        if (saved) {
            try {
                return deepMerge(DEFAULT_CONFIG, JSON.parse(saved));
            } catch (e) {
                console.error('Failed to parse saved config', e);
                return deepMerge({}, DEFAULT_CONFIG);
            }
        }
        return deepMerge({}, DEFAULT_CONFIG);
    }

    function saveConfig() {
        GM_setValue('aiHighlighterConfig', JSON.stringify(config));
        log('Config saved');
    }

    function deepMerge(target, source) {
        const output = { ...target };
        for (let key in source) {
            if (source.hasOwnProperty(key)) {
                if (typeof source[key] === 'object' && !Array.isArray(source[key]) && target[key]) {
                    output[key] = deepMerge(target[key], source[key]);
                } else {
                    output[key] = source[key];
                }
            }
        }
        return output;
    }

    // ==================== DOMAIN EXCLUSION ====================
    function isDomainExcluded() {
        const host = location.hostname;
        return config.excludedDomains.some(pattern => {
            const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
            const regex = new RegExp(`^${regexPattern}$`, 'i');
            return regex.test(host);
        });
    }

    // ==================== HEURISTICS (unchanged) ====================
    const Heuristics = {
        punctuationMonotony(text) {
            const cfg = config.heuristics.punctuationMonotony;
            const words = text.split(/\s+/).filter(w => w.length > 0);
            const wordCount = words.length;
            if (wordCount < 50) return { score: 0, triggers: [] };

            const emDashCount = (text.match(/—/g) || []).length;
            const emDashDensity = (emDashCount / wordCount) * 200;
            let score = 0;
            const triggers = [];

            if (emDashDensity >= cfg.emDashPer200Words) {
                score += 0.6;
                triggers.push(`High em-dash density (${emDashCount} in ${wordCount} words)`);
            }

            const curlyDouble = (text.match(/[“”]/g) || []).length;
            const straightDouble = (text.match(/"/g) || []).length;
            if (curlyDouble > straightDouble * 0.8) {
                score += cfg.curlyQuotesBonus;
                triggers.push('Consistent use of curly quotes (AI hallmark)');
            }

            return { score: Math.min(score, 1.0), triggers };
        },

        homoglyph(text) {
            const cfg = config.heuristics.homoglyph;
            const wordRegex = /\b\w+\b/g;
            let match;
            let suspiciousCount = 0;
            const suspiciousExamples = [];
            while ((match = wordRegex.exec(text)) !== null) {
                const word = match[0];
                if (/[^\x00-\x7F]/.test(word) && /^[A-Za-zÀ-ÿ]+$/.test(word) === false) {
                    suspiciousCount++;
                    if (suspiciousExamples.length < 3) suspiciousExamples.push(word);
                }
            }
            const zwspCount = (text.match(/\u200B/g) || []).length;
            const zwjCount = (text.match(/\u200D/g) || []).length;

            let score = 0;
            const triggers = [];
            if (suspiciousCount > 0) {
                score += Math.min(suspiciousCount * 0.3, 1.0);
                triggers.push(`Found ${suspiciousCount} words with non-ASCII characters (e.g., ${suspiciousExamples.join(', ')})`);
            }
            if (zwspCount > 0) {
                score += 0.8;
                triggers.push(`Detected ${zwspCount} zero-width spaces (invisible watermark)`);
            }
            if (zwjCount > 0) {
                score += 0.8;
                triggers.push(`Detected ${zwjCount} zero-width joiners`);
            }

            return { score: Math.min(score, 1.0), triggers };
        },

        multilingualLeakage(text) {
            const cfg = config.heuristics.multilingualLeakage;
            let score = 0;
            const triggers = [];

            const cjkMatches = text.match(/[\u4E00-\u9FFF]/g) || [];
            if (cjkMatches.length > 0) {
                score += 0.9;
                triggers.push(`Found ${cjkMatches.length} CJK characters in English text`);
            }

            const persianMatches = text.match(/[\u0600-\u06FF]/g) || [];
            if (persianMatches.length > 0) {
                score += 0.9;
                triggers.push(`Found ${persianMatches.length} Persian/Arabic characters`);
            }

            cfg.leakedStrings.forEach(str => {
                if (text.includes(str)) {
                    score += 0.8;
                    triggers.push(`Found known leakage pattern: "${str}"`);
                }
            });

            return { score: Math.min(score, 1.0), triggers };
        },

        vocabulary(text) {
            const cfg = config.heuristics.vocabulary;
            const words = text.split(/\s+/).filter(w => w.length > 0);
            const wordCount = words.length;
            if (wordCount < 50) return { score: 0, triggers: [] };

            const lowerText = text.toLowerCase();
            let hitCount = 0;
            const hitsFound = [];
            cfg.highRiskWords.forEach(term => {
                const count = (lowerText.match(new RegExp(term, 'g')) || []).length;
                if (count > 0) {
                    hitCount += count;
                    hitsFound.push(`${term} (${count})`);
                }
            });

            const densityPer250 = (hitCount / wordCount) * 250;
            let score = 0;
            const triggers = [];
            if (densityPer250 >= cfg.thresholdPer250Words) {
                score = Math.min(densityPer250 / (cfg.thresholdPer250Words * 3), 1.0);
                triggers.push(`High AI vocabulary density: ${hitCount} hits in ${wordCount} words (${densityPer250.toFixed(1)} per 250)`);
                if (hitsFound.length) triggers.push(`Terms: ${hitsFound.join(', ')}`);
            }

            return { score, triggers };
        },

        emojiSignature(text) {
            const cfg = config.heuristics.emojiSignature;
            let score = 0;
            const triggers = [];
            cfg.aiEmojis.forEach(emoji => {
                if (text.includes(emoji)) {
                    score += 0.7;
                    triggers.push(`AI-favored emoji detected: ${emoji}`);
                }
            });
            return { score: Math.min(score, 1.0), triggers };
        },

        formattingArtifacts(text) {
            const cfg = config.heuristics.formattingArtifacts;
            let score = 0;
            const triggers = [];

            const latexPattern = /\\\[|\\\]|\\\(|\\\)/g;
            const latexMatches = text.match(latexPattern) || [];
            if (latexMatches.length > 0) {
                score += 0.8;
                triggers.push(`Found LaTeX delimiters: ${latexMatches.join(' ')}`);
            }

            const lines = text.split('\n');
            let titleCaseLines = 0;
            lines.forEach(line => {
                line = line.trim();
                if (line.length < 5) return;
                const words = line.split(/\s+/);
                const titleWords = words.filter(w => /^[A-Z][a-z]+$/.test(w) || /^[A-Z]+$/.test(w));
                if (titleWords.length >= 2 && titleWords.length / words.length > 0.7) {
                    titleCaseLines++;
                }
            });
            const ratio = titleCaseLines / Math.max(lines.length, 1);
            if (ratio >= cfg.titleCaseThreshold) {
                score += 0.6;
                triggers.push(`High ratio of Title Case headers (${(ratio * 100).toFixed(0)}%)`);
            }

            return { score: Math.min(score, 1.0), triggers };
        },

        structuralUniformity(text) {
            const cfg = config.heuristics.structuralUniformity;
            const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
            if (sentences.length < 5) return { score: 0, triggers: [] };

            const wordCounts = sentences.map(s => s.trim().split(/\s+/).length);
            const [minLen, maxLen] = cfg.uniformSentenceLengthRange;
            const inRange = wordCounts.filter(wc => wc >= minLen && wc <= maxLen).length;
            const uniformity = inRange / sentences.length;

            let score = 0;
            const triggers = [];
            if (uniformity >= cfg.uniformityThreshold) {
                score = uniformity;
                triggers.push(`Sentence length uniformity: ${(uniformity * 100).toFixed(0)}% of sentences are 15-25 words`);
            }

            const threeItemListPattern = /\b(\w+),\s+(\w+),\s+and\s+(\w+)\b/gi;
            const threeItemMatches = text.match(threeItemListPattern) || [];
            if (threeItemMatches.length >= 2) {
                score += 0.3;
                triggers.push(`Found ${threeItemMatches.length} "rule of three" lists`);
            }

            return { score: Math.min(score, 1.0), triggers };
        },

        cannedDisclaimers(text) {
            const cfg = config.heuristics.cannedDisclaimers;
            const lower = text.toLowerCase();
            let score = 0;
            const triggers = [];
            cfg.phrases.forEach(phrase => {
                if (lower.includes(phrase)) {
                    score = 1.0;
                    triggers.push(`Contains AI disclaimer: "${phrase}"`);
                }
            });
            return { score, triggers };
        },

        lackOfSpecificity(text) {
            const cfg = config.heuristics.lackOfSpecificity;
            const words = text.split(/\s+/).filter(w => w.length > 0);
            const wordCount = words.length;
            if (wordCount < 100) return { score: 0, triggers: [] };

            const lower = text.toLowerCase();
            let vagueCount = 0;
            const vagueFound = [];
            cfg.vagueTerms.forEach(term => {
                const count = (lower.match(new RegExp(term, 'g')) || []).length;
                if (count > 0) {
                    vagueCount += count;
                    vagueFound.push(`${term} (${count})`);
                }
            });

            const specificPattern = /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g;
            const specificMatches = text.match(specificPattern) || [];
            const specificCount = specificMatches.length;

            const vaguePer500 = (vagueCount / wordCount) * 500;
            const specificPer500 = (specificCount / wordCount) * 500;

            let score = 0;
            const triggers = [];
            if (vaguePer500 > specificPer500 * cfg.thresholdRatio) {
                score = Math.min(vaguePer500 / (specificPer500 + 1), 1.0);
                triggers.push(`Vague terms (${vagueCount}) outnumber specific entities (${specificCount})`);
                if (vagueFound.length) triggers.push(`Examples: ${vagueFound.slice(0, 3).join(', ')}`);
            }

            return { score, triggers };
        }
    };

    // ==================== SCORING ENGINE ====================
    function analyzeText(text) {
        const results = [];
        let totalWeightedScore = 0;
        let totalWeight = 0;
        const allTriggers = [];

        for (const [name, heuristicFn] of Object.entries(Heuristics)) {
            const cfgHeur = config.heuristics[name];
            if (!cfgHeur || !cfgHeur.enabled) continue;

            try {
                const { score, triggers } = heuristicFn(text);
                const weight = cfgHeur.weight;
                totalWeightedScore += score * weight;
                totalWeight += weight;
                if (triggers.length > 0) {
                    results.push({ name, score, weight, triggers });
                    allTriggers.push(...triggers);
                }
            } catch (e) {
                warn(`Heuristic ${name} error:`, e);
            }
        }

        const normalizedScore = totalWeight > 0 ? (totalWeightedScore / totalWeight) * 10 : 0;
        const finalScore = Math.min(normalizedScore * 10, 100);

        return {
            score: finalScore,
            triggers: allTriggers,
            details: results
        };
    }

    // ==================== BLOCK PROCESSING ====================
    function getBlockElements() {
        const selectors = 'p, div:not(.ai-highlighter-ui):not(.ai-highlight), li, blockquote, h1, h2, h3, h4, h5, h6, section, article, td, th';
        const elements = document.querySelectorAll(selectors);
        const candidates = Array.from(elements).filter(el => {
            const text = el.innerText?.trim() || '';
            if (text.length < 30) return false;
            if (el.closest('.ai-highlighter-ui')) return false;
            return true;
        });
        // Keep only leaf blocks: elements that contain no other candidate blocks
        return candidates.filter(el => {
            // Check if any child element is also a candidate
            return !candidates.some(candidate => candidate !== el && el.contains(candidate));
        });
    }

    function clearHighlights() {
        document.querySelectorAll('.ai-highlight').forEach(el => {
            const parent = el.parentNode;
            while (el.firstChild) {
                parent.insertBefore(el.firstChild, el);
            }
            parent.removeChild(el);
            parent.normalize();
        });
        log('Cleared all highlights');
    }

    function applyHighlights() {
        if (isDomainExcluded()) {
            log('Domain excluded, skipping highlight');
            return;
        }

        if (isApplyingHighlights) return;
        isApplyingHighlights = true;

        try {
            const blocks = getBlockElements();
            log(`Scanning ${blocks.length} block elements`);

            blocks.forEach(block => {
                const text = block.innerText;
                const analysis = analyzeText(text);
                if (analysis.score >= config.threshold) {
                    const wrapper = document.createElement('span');
                    wrapper.className = 'ai-highlight';
                    wrapper.setAttribute('data-ai-evidence', JSON.stringify(analysis.triggers.slice(0, 10)));

                    while (block.firstChild) {
                        wrapper.appendChild(block.firstChild);
                    }
                    block.appendChild(wrapper);

                    log(`Highlighted block with score ${analysis.score.toFixed(1)}`);
                }
            });
        } finally {
            isApplyingHighlights = false;
        }
    }

    // ==================== TOOLTIP (unchanged) ====================
    function initTooltip() {
        if (tooltipEl) return;
        tooltipEl = document.createElement('div');
        tooltipEl.className = 'ai-highlighter-tooltip';
        tooltipEl.style.cssText = `
            position: fixed;
            background: #333;
            color: #fff;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 13px;
            max-width: 350px;
            z-index: 2147483647;
            pointer-events: none;
            box-shadow: 0 3px 10px rgba(0,0,0,0.3);
            display: none;
        `;
        document.body.appendChild(tooltipEl);

        document.addEventListener('mouseover', e => {
            const target = e.target.closest('.ai-highlight');
            if (!target) {
                tooltipEl.style.display = 'none';
                return;
            }

            const evidenceRaw = target.getAttribute('data-ai-evidence');
            if (!evidenceRaw) return;

            try {
                const evidence = JSON.parse(evidenceRaw);
                let html = '<strong style="color:#FFEB3B">⚠️ Likely AI generated</strong><ul style="margin:5px 0 0 15px; padding:0;">';
                evidence.slice(0, 5).forEach(item => {
                    html += `<li>${escapeHtml(item)}</li>`;
                });
                if (evidence.length > 5) html += `<li>... and ${evidence.length - 5} more indicators</li>`;
                html += '</ul>';
                tooltipEl.innerHTML = html;
                tooltipEl.style.display = 'block';
            } catch (e) {
                tooltipEl.textContent = 'Likely AI generated';
                tooltipEl.style.display = 'block';
            }
        });

        document.addEventListener('mousemove', e => {
            if (tooltipEl.style.display === 'block') {
                const x = e.clientX + 15;
                const y = e.clientY + 10;
                tooltipEl.style.left = x + 'px';
                tooltipEl.style.top = y + 'px';
            }
        });

        document.addEventListener('mouseout', e => {
            if (e.target.closest('.ai-highlight')) {
                tooltipEl.style.display = 'none';
            }
        });
    }

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // ==================== MUTATION OBSERVER (FIXED) ====================
    function startObserver() {
        if (observer) observer.disconnect();
        observer = new MutationObserver(mutations => {
            // Ignore if we're already applying highlights or if mutations are our own
            if (isApplyingHighlights) return;

            const shouldRescan = mutations.some(m => {
                if (m.type !== 'childList') return false;
                // Skip if target or added nodes belong to our UI or highlights
                if (m.target.closest && (m.target.closest('.ai-highlighter-ui') || m.target.closest('.ai-highlight'))) {
                    return false;
                }
                // Check added nodes
                for (let node of m.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.closest && (node.closest('.ai-highlighter-ui') || node.closest('.ai-highlight'))) {
                            continue;
                        }
                        // Only rescan if the added node might contain text blocks
                        if (node.querySelector) {
                            const hasTextBlocks = node.querySelector('p, div, li, blockquote, h1, h2, h3, h4, h5, h6, section, article, td, th');
                            if (hasTextBlocks) return true;
                        }
                    }
                }
                return false;
            });

            if (shouldRescan) {
                scheduleScan();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    function scheduleScan() {
        if (scanTimer) clearTimeout(scanTimer);
        scanTimer = setTimeout(() => {
            if (isDomainExcluded() || isApplyingHighlights) return;
            log('Debounced rescan triggered');
            clearHighlights();
            applyHighlights();
            scanTimer = null;
        }, config.debounceMs);
    }

    // ==================== SETTINGS UI ====================
    function createSettingsPanel() {
        // Remove existing if any
        if (settingsPanel) settingsPanel.remove();

        settingsPanel = document.createElement('div');
        settingsPanel.className = 'ai-highlighter-ui';
        settingsPanel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 380px;
            max-height: 90vh;
            overflow-y: auto;
            background: #fff;
            border: 1px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.3);
            z-index: 2147483646;
            padding: 16px;
            font-family: system-ui, sans-serif;
            font-size: 14px;
            color: #333;
        `;

        let html = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
                <h3 style="margin:0;">🤖 AI Highlighter Settings</h3>
                <button id="ai-close-settings" style="background:none; border:none; font-size:20px; cursor:pointer;">&times;</button>
            </div>

            <div style="margin-bottom:15px;">
                <label>Global Threshold: <span id="ai-threshold-value">${config.threshold}</span></label>
                <input type="range" id="ai-threshold" min="0" max="100" value="${config.threshold}" style="width:100%;">
            </div>

            <div style="margin-bottom:15px;">
                <label>Rescan Debounce (ms):</label>
                <input type="number" id="ai-debounce" value="${config.debounceMs}" min="100" max="5000" step="100" style="width:80px;">
            </div>

            <div style="margin-bottom:15px;">
                <label style="display:flex; align-items:center;">
                    <input type="checkbox" id="ai-debug" ${config.debug ? 'checked' : ''}> Debug Mode (console logging)
                </label>
            </div>

            <div style="margin-bottom:15px;">
                <label>Excluded Domains (one per line, * wildcard):</label>
                <textarea id="ai-excluded" rows="3" style="width:100%;">${config.excludedDomains.join('\n')}</textarea>
            </div>

            <div style="margin-bottom:15px;">
                <h4 style="margin:10px 0;">Heuristics</h4>
                <div id="ai-heuristics-list"></div>
            </div>

            <div style="display:flex; gap:10px;">
                <button id="ai-exclude-site" style="padding:8px 16px; background:#f44336; color:white; border:none; border-radius:4px; cursor:pointer;">Exclude This Site</button>
                <button id="ai-save-settings" style="padding:8px 16px; background:#4CAF50; color:white; border:none; border-radius:4px; cursor:pointer;">Save & Rescan</button>
                <button id="ai-rescan" style="padding:8px 16px; background:#2196F3; color:white; border:none; border-radius:4px; cursor:pointer;">Rescan Now</button>
            </div>
        `;

        settingsPanel.innerHTML = html;
        document.body.appendChild(settingsPanel);

        // Helper to render heuristic config panel
        function renderHeuristicConfig(name, cfgHeur) {
            const container = document.createElement('div');
            container.style.marginLeft = '25px';
            container.style.marginTop = '5px';
            container.style.padding = '5px';
            container.style.backgroundColor = '#f5f5f5';
            container.style.borderRadius = '4px';
            container.style.display = 'none'; // hidden by default

            const paramNames = Object.keys(cfgHeur).filter(k => k !== 'enabled' && k !== 'weight');
            if (paramNames.length === 0) {
                container.innerHTML = '<em>No adjustable parameters</em>';
                return container;
            }

            paramNames.forEach(param => {
                const value = cfgHeur[param];
                const div = document.createElement('div');
                div.style.marginBottom = '8px';

                const label = document.createElement('label');
                label.style.display = 'block';
                label.style.fontSize = '12px';
                label.textContent = param + ':';

                let input;
                if (typeof value === 'boolean') {
                    input = document.createElement('input');
                    input.type = 'checkbox';
                    input.checked = value;
                    input.dataset.heuristic = name;
                    input.dataset.param = param;
                    input.dataset.type = 'boolean';
                    label.appendChild(input);
                    label.appendChild(document.createTextNode(' Enabled'));
                } else if (typeof value === 'number') {
                    input = document.createElement('input');
                    input.type = 'number';
                    input.value = value;
                    input.step = param.includes('Threshold') || param.includes('Ratio') ? '0.1' : '1';
                    input.min = '0';
                    input.style.width = '80px';
                    input.dataset.heuristic = name;
                    input.dataset.param = param;
                    input.dataset.type = 'number';
                    div.appendChild(label);
                    div.appendChild(input);
                } else if (Array.isArray(value)) {
                    input = document.createElement('textarea');
                    input.value = value.join('\n');
                    input.rows = 3;
                    input.style.width = '100%';
                    input.style.fontSize = '12px';
                    input.dataset.heuristic = name;
                    input.dataset.param = param;
                    input.dataset.type = 'array';
                    div.appendChild(label);
                    div.appendChild(input);
                } else {
                    // fallback string input
                    input = document.createElement('input');
                    input.type = 'text';
                    input.value = value;
                    input.style.width = '100%';
                    input.dataset.heuristic = name;
                    input.dataset.param = param;
                    input.dataset.type = 'string';
                    div.appendChild(label);
                    div.appendChild(input);
                }

                if (input && !(typeof value === 'boolean')) {
                    div.appendChild(input);
                }
                container.appendChild(div);
            });

            return container;
        }

        // Populate heuristics toggles and params
        const heurContainer = document.getElementById('ai-heuristics-list');
        for (const [name, cfgHeur] of Object.entries(config.heuristics)) {
            const div = document.createElement('div');
            div.style.marginBottom = '8px';
            div.style.borderBottom = '1px solid #eee';
            div.style.paddingBottom = '5px';

            const label = document.createElement('label');
            label.style.display = 'flex';
            label.style.alignItems = 'center';
            label.style.cursor = 'pointer';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.dataset.heuristic = name;
            checkbox.checked = cfgHeur.enabled;
            checkbox.style.marginRight = '6px';

            const span = document.createElement('span');
            span.textContent = formatHeuristicName(name) + ` (weight: ${cfgHeur.weight})`;
            span.style.flex = '1';

            label.appendChild(checkbox);
            label.appendChild(span);
            div.appendChild(label);

            // Add toggle link and config panel
            const toggleLink = document.createElement('a');
            toggleLink.href = '#';
            toggleLink.textContent = '⚙️';
            toggleLink.style.marginLeft = '20px';
            toggleLink.style.fontSize = '12px';
            toggleLink.style.textDecoration = 'none';

            const configPanel = renderHeuristicConfig(name, cfgHeur);
            div.appendChild(configPanel);

            toggleLink.onclick = (e) => {
                e.preventDefault();
                configPanel.style.display = configPanel.style.display === 'none' ? 'block' : 'none';
            };

            div.appendChild(toggleLink);
            heurContainer.appendChild(div);
        }

        // Event listeners
        document.getElementById('ai-threshold').addEventListener('input', e => {
            document.getElementById('ai-threshold-value').textContent = e.target.value;
        });

        document.getElementById('ai-close-settings').addEventListener('click', () => {
            settingsPanel.remove();
            settingsPanel = null;
        });

        document.getElementById('ai-save-settings').addEventListener('click', () => {
            // Gather values
            config.threshold = parseInt(document.getElementById('ai-threshold').value);
            config.debounceMs = parseInt(document.getElementById('ai-debounce').value);
            config.debug = document.getElementById('ai-debug').checked;
            config.excludedDomains = document.getElementById('ai-excluded').value.split('\n').map(s => s.trim()).filter(s => s);

            // Heuristics enabled states
            document.querySelectorAll('#ai-heuristics-list input[type=checkbox]').forEach(cb => {
                const name = cb.dataset.heuristic;
                if (config.heuristics[name]) {
                    config.heuristics[name].enabled = cb.checked;
                }
            });

            // Update advanced heuristic parameters
            document.querySelectorAll('#ai-heuristics-list input[data-heuristic], #ai-heuristics-list textarea[data-heuristic]').forEach(input => {
                const heuristicName = input.dataset.heuristic;
                const paramName = input.dataset.param;
                const type = input.dataset.type;
                let value;
                if (type === 'boolean') {
                    value = input.checked;
                } else if (type === 'number') {
                    value = parseFloat(input.value) || 0;
                } else if (type === 'array') {
                    value = input.value.split('\n').map(s => s.trim()).filter(s => s);
                } else {
                    value = input.value;
                }

                if (config.heuristics[heuristicName]) {
                    config.heuristics[heuristicName][paramName] = value;
                }
            });

            saveConfig();
            log('Config saved', config);

            // Rescan
            clearHighlights();
            applyHighlights();
            settingsPanel.remove();
            settingsPanel = null;
        });

        document.getElementById('ai-rescan').addEventListener('click', () => {
            clearHighlights();
            applyHighlights();
        });

        document.getElementById('ai-exclude-site').addEventListener('click', () => {
            const currentHost = location.hostname;
            if (!config.excludedDomains.includes(currentHost)) {
                config.excludedDomains.push(currentHost);
                document.getElementById('ai-excluded').value = config.excludedDomains.join('\n');
                saveConfig();
                if (config.debug) log(`Added ${currentHost} to excluded domains`);
                clearHighlights();
                settingsPanel.remove();
                settingsPanel = null;
                GM_notification({ text: `AI Highlighter: Excluded ${currentHost}`, timeout: 2000 });
            } else {
                alert('This site is already excluded.');
            }
        });
    }

    function formatHeuristicName(name) {
        return name.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
    }

    // ==================== INITIALIZATION ====================
    function init() {
        log('Initializing AI Highlighter');

        // UPDATED: Softer highlight color with dark text
        GM_addStyle(`
            .ai-highlight {
                background-color: #fff9c4 !important;
                color: #1e1e1e !important;
                cursor: help !important;
                border-radius: 2px;
                transition: background-color 0.2s;
            }
            .ai-highlight:hover {
                background-color: #fff176 !important;
            }
            .ai-highlighter-tooltip {
                font-family: system-ui, sans-serif !important;
                line-height: 1.4 !important;
            }
        `);

        GM_registerMenuCommand('⚙️ AI Highlighter Settings', () => {
            createSettingsPanel();
        });

        GM_registerMenuCommand('🔍 Rescan Page', () => {
            clearHighlights();
            applyHighlights();
            if (config.debug) GM_notification({ text: 'AI Highlighter: Rescan complete', timeout: 2000 });
        });

        if (!isDomainExcluded()) {
            setTimeout(() => {
                applyHighlights();
                initTooltip();
                startObserver();
            }, 1000);
        } else {
            log('Domain excluded, skipping scan');
        }

        // SPA navigation handling (optional)
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                setTimeout(() => {
                    if (!isDomainExcluded()) {
                        clearHighlights();
                        applyHighlights();
                    }
                }, 1000);
            }
        }).observe(document, { subtree: true, childList: true });
    }

    // Start
    init();
})();