Greasy Fork

Greasy Fork is available in English.

🌟 AI 智能词组

利用 NLP 智能提取网页英文词组。彻底解决单词拆解问题,点击高亮处即可查询【整个短语】的精准释义,内置权威词典直达。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         🌟 AI 智能词组
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  利用 NLP 智能提取网页英文词组。彻底解决单词拆解问题,点击高亮处即可查询【整个短语】的精准释义,内置权威词典直达。
// @author       Gemini
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://unpkg.com/@popperjs/core@2
// @require      https://unpkg.com/tippy.js@6
// @require      https://cdn.jsdelivr.net/npm/[email protected]/builds/compromise.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js
// @connect      translate.googleapis.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. 全局样式与 UI
    // ==========================================
    GM_addStyle(`
        .tm-phrase {
            background-color: rgba(76, 175, 80, 0.2); 
            border-bottom: 2px dashed #4CAF50;
            border-radius: 2px;
            cursor: pointer;
            padding: 0 2px;
            transition: background-color 0.2s;
            color: inherit !important;
            position: relative;
        }
        /* 屏蔽子元素事件,防止与单词翻译脚本冲突 */
        .tm-phrase * { pointer-events: none !important; }

        .tm-phrase:hover, .tm-phrase[aria-expanded="true"] {
            background-color: rgba(76, 175, 80, 0.45);
            color: #1b5e20 !important;
        }

        #tm-nlp-fab {
            position: fixed; bottom: 30px; right: 20px; width: 50px; height: 50px;
            background-color: #673AB7; color: white; border-radius: 50%;
            display: flex; align-items: center; justify-content: center;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3); cursor: pointer;
            z-index: 2147483647; font-size: 24px; user-select: none;
            transition: transform 0.3s, background-color 0.3s;
        }
        #tm-nlp-fab:hover { transform: scale(1.1); }
        #tm-nlp-fab.processing { animation: tm-spin 1.5s linear infinite; background-color: #FF9800; }
        @keyframes tm-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

        #tm-nlp-toast {
            position: fixed; bottom: 90px; right: 20px; background: rgba(0,0,0,0.8); color: #fff;
            padding: 10px 15px; border-radius: 8px; font-size: 14px; z-index: 2147483647;
            opacity: 0; pointer-events: none; transition: opacity 0.3s;
        }

        /* 弹窗高级样式 */
        .tippy-box[data-theme~='light-border'] { background-color: #fff; color: #333; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 14px rgba(0,0,0,0.15); }
        .tippy-content { padding: 8px; }
        .dict-popup { text-align: left; font-size: 14px; line-height: 1.4; max-width: 320px; min-width: 220px; color: #333; }
        .dict-head-word { font-weight: bold; color: #673AB7; font-size: 18px; line-height: 1.2; } 
        .tm-badge { background-color: #673AB7; color: white; font-size: 11px; padding: 2px 6px; border-radius: 12px; margin-left: 6px; font-weight: normal; }
        .dict-speaker-btn { display: inline-flex; cursor: pointer; color: #1976D2; padding: 2px; border-radius: 50%; transition: background 0.2s; margin-left: 4px; }
        .dict-speaker-btn:hover { background-color: rgba(25, 118, 210, 0.1); }
        .dict-speaker-btn svg { width: 18px; height: 18px; pointer-events: none; }
        .dict-speaker-btn.playing { color: #E91E63; animation: tm-pulse 1s infinite; }
        .dict-basic-trans { font-size: 15px; color: #222; font-weight: 500; margin-top: 6px; }
        .dict-loading { color: #666; font-style: italic; font-size: 13px; padding: 5px; }

        @keyframes tm-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.15); } 100% { transform: scale(1); } }
    `);

    // ==========================================
    // 2. TTS 与翻译请求模块
    // ==========================================
    let globalAudioPlayer = null;
    let currentPlayingBtn = null;

    function initAudioPlayer() {
        if (!globalAudioPlayer) {
            globalAudioPlayer = document.createElement('audio');
            globalAudioPlayer.style.display = 'none';
            document.body.appendChild(globalAudioPlayer);
        }
    }

    function playTTS(text, btn) {
        if (currentPlayingBtn && currentPlayingBtn !== btn) currentPlayingBtn.classList.remove('playing');
        currentPlayingBtn = btn;
        btn.classList.add('playing');

        const url = `https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=en&q=${encodeURIComponent(text)}`;
        globalAudioPlayer.src = url;
        
        globalAudioPlayer.onended = () => { if (currentPlayingBtn) currentPlayingBtn.classList.remove('playing'); };
        globalAudioPlayer.onerror = () => { if (currentPlayingBtn) currentPlayingBtn.classList.remove('playing'); };
        globalAudioPlayer.play().catch(e => btn.classList.remove('playing'));
    }

    function fetchTranslation(text) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=zh-CN&dt=t&dt=bd&q=${encodeURIComponent(text)}`,
                onload: (res) => {
                    if (res.status === 200) resolve(JSON.parse(res.responseText));
                    else reject(new Error('Translate error'));
                },
                onerror: reject
            });
        });
    }

    function showToast(msg) {
        let toast = document.getElementById('tm-nlp-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'tm-nlp-toast';
            document.body.appendChild(toast);
        }
        toast.innerText = msg;
        toast.style.opacity = '1';
        setTimeout(() => toast.style.opacity = '0', 3000);
    }

    // ==========================================
    // 3. NLP 语法结构挖掘与精准绑定引擎
    // ==========================================
    let tippyInstances = [];
    let markInstance = null;

    function cleanAndFilterPhrase(phrase) {
        const cleaned = phrase.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, "").replace(/\s{2,}/g, " ").trim().toLowerCase();
        if (!cleaned.includes(' ') || cleaned.length <= 3 || cleaned.length > 40) return null;

        const copulasAndModals = ['is', 'am', 'are', 'was', 'were', 'be', 'been', 'being', 'can', 'could', 'shall', 'should', 'will', 'would', 'may', 'might', 'must', 'has', 'have', 'had', 'do', 'does', 'did', 'it', 'there'];
        if (copulasAndModals.includes(cleaned.split(' ')[0])) return null;
        return cleaned;
    }

    function extractAndHighlight() {
        if (!window.nlp || !window.Mark) { alert("依赖库未加载完毕,请稍后再试!"); return; }

        const fab = document.getElementById('tm-nlp-fab');
        fab.classList.add('processing');
        fab.innerHTML = '⚙️';

        setTimeout(() => {
            const mainContent = document.body.innerText;
            const doc = nlp(mainContent);
            
            const grammarRules = [
                '#PhrasalVerb', 
                '#Idiom',       
                '#Verb (up|down|in|out|on|off|over|under|away|back|forward|through|with|about|at|from|into|of|to|for)',
                '#Verb #Pronoun (up|down|in|out|on|off|over|under|away|back|forward|through)',
                '#Verb (a|an|the) #Adjective? #Noun',
                '#Adjective (in|of|with|for|to|about|at|on|from)'
            ];

            let rawPhrases = [];
            grammarRules.forEach(rule => { rawPhrases = rawPhrases.concat(doc.match(rule).out('array')); });

            const validPhrases = rawPhrases.map(cleanAndFilterPhrase).filter(Boolean);
            const uniquePhrases = [...new Set(validPhrases)];

            if (uniquePhrases.length === 0) {
                fab.classList.remove('processing'); fab.innerHTML = '🪄';
                showToast("未检测到合适的英语词组。"); return;
            }

            if (!markInstance) markInstance = new Mark(document.body);
            
            // 异步分批高亮机制:解决跨元素断层问题
            function markNextPhrase(index) {
                if (index >= uniquePhrases.length) {
                    bindTippyToPhrases();
                    fab.classList.remove('processing'); fab.innerHTML = '✅';
                    showToast(`🎯 提取成功!发现了 ${uniquePhrases.length} 个词组搭配。`);
                    setTimeout(() => fab.innerHTML = '🪄', 2000);
                    return;
                }
                
                const phrase = uniquePhrases[index];
                markInstance.mark(phrase, {
                    className: "tm-phrase",
                    accuracy: "exactly",
                    separateWordSearch: false,
                    acrossElements: true, // 核心穿透能力
                    exclude: ["script", "style", "noscript", ".tippy-box", "textarea", "input", "#tm-nlp-toast"],
                    each: function(node) {
                        // 无论被切割成多少块,全都绑定同一个完整的短语!
                        node.setAttribute('data-full-phrase', phrase);
                    },
                    done: () => {
                        // 避免阻塞主线程卡顿
                        if (index % 10 === 0) setTimeout(() => markNextPhrase(index + 1), 0);
                        else markNextPhrase(index + 1);
                    }
                });
            }

            markInstance.unmark({
                className: "tm-phrase",
                done: () => markNextPhrase(0)
            });
        }, 150);
    }

    // ==========================================
    // 4. 短语级专业查词弹窗
    // ==========================================
    function bindTippyToPhrases() {
        if (tippyInstances.length > 0) { tippyInstances.forEach(t => t.destroy()); tippyInstances = []; }
        
        tippyInstances = tippy('.tm-phrase', {
            trigger: 'click', 
            interactive: true,
            theme: 'light-border',
            placement: 'bottom',
            appendTo: () => document.body,
            maxWidth: 350,
            onShow(instance) {
                // 【核心逻辑】:不读取innerText,直接读取刚才锚定的完整词组
                const phrase = instance.reference.getAttribute('data-full-phrase');
                if (!phrase) return;

                const loadingDiv = document.createElement('div');
                loadingDiv.className = 'dict-popup';
                loadingDiv.innerHTML = '<div class="dict-loading">🔍 提取短语涵义中...</div>';
                ['mousedown', 'touchstart', 'click'].forEach(evt => loadingDiv.addEventListener(evt, e => e.stopPropagation(), { capture: true }));
                instance.setContent(loadingDiv);
                
                fetchTranslation(phrase).then((transData) => {
                    // 确保获取完整的翻译拼接
                    let basicTrans = '';
                    if (transData[0] && transData[0].length > 0) {
                        transData[0].forEach(item => { if (item[0]) basicTrans += item[0]; });
                    }
                    if (!basicTrans) basicTrans = '无翻译结果';
                    
                    const contentDiv = document.createElement('div');
                    contentDiv.className = 'dict-popup';
                    contentDiv.innerHTML = `
                        <div class="dict-header-row" style="border:none; margin:0; padding:0; padding-bottom: 8px; border-bottom: 1px dashed #e0e0e0;">
                            <div class="dict-word-line" style="margin-bottom: 4px; display:flex; align-items:center; flex-wrap:wrap;">
                                <span class="dict-head-word" style="margin-right: 4px;">${phrase}</span>
                                <span class="tm-badge">短语涵义</span>
                                <span class="dict-speaker-btn" data-phrase="${phrase.replace(/"/g, '&quot;')}" title="Pronounce">
                                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>
                                </span>
                            </div>
                            <div class="dict-basic-trans">💡 ${basicTrans}</div>
                        </div>
                        <div class="tm-dict-links" style="margin-top: 8px; display: flex; gap: 8px; font-size: 12px;">
                            <a href="https://dict.youdao.com/result?word=${encodeURIComponent(phrase)}&lang=en" target="_blank" style="color: #673AB7; text-decoration: none; background: #f3e5f5; padding: 4px 8px; border-radius: 4px; font-weight: bold;">📖 有道权威解析</a>
                            <a href="https://dictionary.cambridge.org/dictionary/english-chinese-simplified/${encodeURIComponent(phrase.replace(/\s+/g, '-'))}" target="_blank" style="color: #673AB7; text-decoration: none; background: #f3e5f5; padding: 4px 8px; border-radius: 4px; font-weight: bold;">📘 剑桥词典</a>
                        </div>
                    `;

                    ['mousedown', 'touchstart', 'click'].forEach(evt => contentDiv.addEventListener(evt, e => e.stopPropagation(), { capture: true }));

                    const btn = contentDiv.querySelector('.dict-speaker-btn');
                    if (btn) {
                        btn.addEventListener('click', (e) => {
                            e.preventDefault(); e.stopPropagation();
                            const text = btn.getAttribute('data-phrase');
                            if (!text || btn.classList.contains('playing')) return;
                            playTTS(text, btn);
                        });
                    }
                    instance.setContent(contentDiv);
                }).catch(() => {
                    instance.setContent('<div style="padding:5px;">网络错误,无法翻译。</div>');
                });
            }
        });
    }

    // ==========================================
    // 5. 初始化与 UI 挂载
    // ==========================================
    function createUI() {
        initAudioPlayer();
        const fab = document.createElement('div');
        fab.id = 'tm-nlp-fab';
        fab.innerHTML = '🪄';
        fab.title = "点击智能分析:自动提取页面动词短语与习语";
        document.body.appendChild(fab);

        fab.addEventListener('click', (e) => {
            e.stopPropagation();
            extractAndHighlight();
        });
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(createUI, 500);
    } else {
        window.addEventListener('DOMContentLoaded', () => setTimeout(createUI, 500));
    }

})();