Ask AI

Search selected text across AI tools (No popup warnings, no double-tabs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// @license      MIT
// ==UserScript==
// @name         Ask AI
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @description  Search selected text across AI tools (No popup warnings, no double-tabs)
// @match        *://*/*
// @run-at       document-end
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
    "use strict";

    const buttonId = "ask-ai-button";
    const menuId = "ask-ai-menu";
    const promptId = "ask-ai-prompt";

    let mouseX = 0, mouseY = 0;
    let scrollBaseY = 0;
    let selectionTimeout = null;
    let activeSelectionText = "";
    let menuFiring = false;

    const AIs = [
        { name: "Perplexity",   url: t => `https://www.perplexity.ai/search?q=${t}` },
        { name: "Google Search", url: t => `https://www.google.com/search?q=${t}&udm=50` },
        { name: "DeepSeek",     url: () => `https://chat.deepseek.com/`, copy: true },
        { name: "Claude",       url: t => `https://claude.ai/new?q=${t}` },
        { name: "Grok",         url: t => `https://grok.com/?q=${t}` },
        { name: "ChatGPT",      url: t => `https://chatgpt.com/?q=${t}` },
        { name: "Gemini",       url: () => `https://gemini.google.com/app`, copy: true },
    ];

    const PRESETS = [
        "Explain", "Elaborate", "ELI5", "Synonyms",
        "Summarize", "Examples", "Translate", "Define",
        "Etymology", "Pros & Cons", "Counterarguments", "Fix grammar",
        "Rewrite formally", "Rewrite casually", "Shorter", "Longer",
        "Fact-check", "Historical context", "Real-world examples", "Compare & contrast",
        "Key takeaways", "Quiz me on this",
    ];

    function trackMouse(e) {
        mouseX = e.clientX;
        mouseY = e.clientY;
    }

    function handleKeyDown(e) {
        const tag = e.target && e.target.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA" || (e.target && e.target.isContentEditable)) return;

        if (e.altKey && e.key.toLowerCase() === "q") {
            e.preventDefault();
            const text = getSelectionText();
            if (!text) return;
            const existing = document.getElementById(menuId);
            if (existing) { removeMenu(); return; }
            const bounds = getSelectionBounds();
            const px = bounds ? bounds.x + 6 : mouseX + 12;
            const py = bounds ? bounds.y - 6 : mouseY + 12;
            showMenu(px, py, text);
            return;
        }

        const digitMatch = e.code && e.code.match(/^Digit([1-7])$/);
        if (digitMatch && (e.shiftKey || document.getElementById(menuId))) {
            const index = parseInt(digitMatch[1], 10) - 1;
            const ai = AIs[index];
            const text = getSelectionText() || activeSelectionText;
            if (!ai || !text) return;

            if (e.shiftKey) {
                e.preventDefault();
                const bounds = getSelectionBounds();
                const px = bounds ? bounds.x + 6 : mouseX + 12;
                const py = bounds ? bounds.y - 6 : mouseY + 12;
                showPrompt(ai, text, px, py);
            } else if (document.getElementById(menuId)) {
                openAI(ai, text);
                removeMenu();
                hideButton();
            }
        }
    }

    function handleClick() {
        removeMenu();
        removePrompt();
        activeSelectionText = "";
        setTimeout(createButton, 0);
    }

    function handleScroll() {
        if (Math.abs(window.scrollY - scrollBaseY) > 50) {
            removeMenu();
            removePrompt();
            hideButton();
        }
    }

    function handleSelectionChange() {
        clearTimeout(selectionTimeout);
        selectionTimeout = setTimeout(createButton, 120);
    }

    function cleanup() {
        document.removeEventListener("mousemove", trackMouse);
        document.removeEventListener("keydown", handleKeyDown);
        document.removeEventListener("click", handleClick);
        window.removeEventListener("scroll", handleScroll);
        document.removeEventListener("selectionchange", handleSelectionChange);
        window.removeEventListener("load", createButton);
        clearTimeout(selectionTimeout);
        removeMenu();
        removePrompt();
        hideButton();
    }

    function getSelectionText() {
        const sel = document.getSelection();
        if (!sel) return null;
        const text = sel.toString().trim();
        return (!text || text.length > 1000) ? null : text;
    }

    function getSelectionBounds() {
        const sel = document.getSelection();
        if (!sel || sel.rangeCount === 0) return null;
        try {
            const range = sel.getRangeAt(0);
            const rect = range.getBoundingClientRect();
            return (rect.width > 0 || rect.height > 0) ? { x: rect.right, y: rect.top } : null;
        } catch (_) {
            return null;
        }
    }

    function removeMenu() {
        const m = document.getElementById(menuId);
        if (m) m.remove();
        activeSelectionText = "";
    }

    function removePrompt() {
        const p = document.getElementById(promptId);
        if (p) p.remove();
        activeSelectionText = "";
    }

    function hideButton() {
        const btn = document.getElementById(buttonId);
        if (btn) btn.style.display = "none";
    }

    function clampToViewport(el, preferredX, preferredY) {
        requestAnimationFrame(() => {
            if (!el.parentNode) return;
            const pad = 8;
            const rect = el.getBoundingClientRect();
            let x = Math.max(pad, Math.min(preferredX, window.innerWidth - rect.width - pad));
            let y = Math.max(pad, Math.min(preferredY, window.innerHeight - rect.height - pad));
            el.style.left = `${x}px`;
            el.style.top = `${y}px`;
        });
    }

    function blockMiddleClick(el) {
        ["mousedown", "mouseup", "click", "auxclick"].forEach(type => {
            el.addEventListener(type, (e) => {
                if (e.button === 1) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            });
        });
    }

    async function copyText(text) {
        if (typeof GM_setClipboard === "function") {
            try {
                GM_setClipboard(text, "text");
                return true;
            } catch (_) {}
        }

        if (navigator.clipboard?.writeText) {
            try {
                await navigator.clipboard.writeText(text);
                return true;
            } catch (_) {}
        }

        try {
            const ta = document.createElement("textarea");
            ta.value = text;
            ta.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none;";
            document.body.appendChild(ta);
            ta.select();
            const success = document.execCommand("copy");
            document.body.removeChild(ta);
            return success;
        } catch (_) {
            return false;
        }
    }

    // --- SIMPLE openAI: Only opens in new tab, no fallbacks, no alerts ---
    function openAI(ai, text) {
        if (!text) return;
        if (ai.copy) {
            copyText(text).catch(() => {}); // Silently try to copy (no alerts)
        }
        const url = ai.copy ? ai.url() : ai.url(encodeURIComponent(text));
        window.open(url, "_blank", "noopener,noreferrer");
    }

    function showPrompt(ai, text, anchorX, anchorY) {
        removePrompt();
        scrollBaseY = window.scrollY;
        activeSelectionText = text;

        let includeContext = false;
        let firing = false;

        const box = document.createElement("div");
        box.id = promptId;
        box.tabIndex = -1;
        box.setAttribute("role", "dialog");
        box.setAttribute("aria-label", "Customize AI prompt");
        Object.assign(box.style, {
            position: "fixed",
            zIndex: "9999",
            left: `${anchorX}px`,
            top: `${anchorY}px`,
            backgroundColor: "#111",
            border: "1px solid #333",
            borderRadius: "10px",
            padding: "10px",
            boxShadow: "0 4px 16px rgba(0,0,0,0.5)",
            fontFamily: "system-ui, sans-serif",
            fontSize: "11px",
            width: "260px",
            userSelect: "none",
        });

        box.addEventListener("click", (e) => {
            e.stopPropagation();
            if (e.target !== input && !e.target.closest("button, input")) box.focus();
        });
        blockMiddleClick(box);

        const contextToggle = document.createElement("div");
        contextToggle.textContent = "🌐 Context: OFF (press C)";
        contextToggle.setAttribute("role", "button");
        contextToggle.setAttribute("aria-label", "Toggle page context");
        Object.assign(contextToggle.style, {
            fontSize: "10px", color: "#888", marginBottom: "6px",
            cursor: "pointer", textAlign: "center", padding: "3px",
            borderRadius: "4px", transition: "color 0.15s",
        });

        const refreshContextLabel = () => {
            contextToggle.textContent = includeContext ? "🌐 Context: ON (press C)" : "🌐 Context: OFF (press C)";
            contextToggle.style.color = includeContext ? "#4fc" : "#888";
        };

        contextToggle.addEventListener("mouseenter", () => contextToggle.style.color = "#ccc");
        contextToggle.addEventListener("mouseleave", refreshContextLabel);
        contextToggle.addEventListener("click", (e) => {
            e.stopPropagation();
            includeContext = !includeContext;
            refreshContextLabel();
        });
        box.appendChild(contextToggle);

        const grid = document.createElement("div");
        Object.assign(grid.style, {
            display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4px", marginBottom: "8px"
        });
        PRESETS.forEach(preset => {
            const btn = document.createElement("button");
            btn.textContent = preset;
            btn.setAttribute("aria-label", `Use preset: ${preset}`);
            Object.assign(btn.style, {
                padding: "4px 6px", borderRadius: "5px", border: "1px solid #333",
                backgroundColor: "#1a1a1a", color: "#ccc", cursor: "pointer",
                fontSize: "11px", textAlign: "left", transition: "background 0.1s"
            });
            btn.addEventListener("mouseenter", () => btn.style.backgroundColor = "#2a2a2a");
            btn.addEventListener("mouseleave", () => btn.style.backgroundColor = "#1a1a1a");
            btn.addEventListener("click", (e) => {
                e.stopPropagation();
                input.value = preset + ": ";
                input.focus();
                input.setSelectionRange(input.value.length, input.value.length);
            });
            grid.appendChild(btn);
        });

        const input = document.createElement("input");
        input.type = "text";
        input.placeholder = "Custom prefix...";
        input.setAttribute("aria-label", "Prompt prefix");
        Object.assign(input.style, {
            width: "100%", padding: "6px 8px", borderRadius: "6px", border: "1px solid #333",
            backgroundColor: "#1a1a1a", color: "#eee", fontSize: "12px", outline: "none",
            boxSizing: "border-box", marginBottom: "8px"
        });
        blockMiddleClick(input);

        const send = document.createElement("button");
        send.textContent = "Send";
        send.setAttribute("aria-label", "Send to AI");
        Object.assign(send.style, {
            width: "100%", padding: "6px", borderRadius: "6px", border: "none",
            backgroundColor: "#222", color: "#eee", cursor: "pointer", fontSize: "12px"
        });

        function fire() {
            if (firing) return;
            firing = true;
            const prefix = input.value.trim();
            let query = prefix ? `${prefix}: ${text}` : text;
            if (includeContext) {
                query += `\n\n---\nSource: "${document.title}"\n${location.href}`;
            }
            openAI(ai, query);
            removePrompt();
            removeMenu();
            hideButton();
            setTimeout(() => { firing = false; }, 500);
        }

        send.addEventListener("click", (e) => { e.stopPropagation(); fire(); });
        input.addEventListener("keydown", (e) => {
            if (e.key === "Enter") { e.preventDefault(); fire(); }
            else if (e.key === "Escape") { e.preventDefault(); removePrompt(); }
            e.stopPropagation();
        });

        box.addEventListener("keydown", (e) => {
            if (e.key === "c" && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && document.activeElement !== input) {
                e.preventDefault();
                e.stopPropagation();
                includeContext = !includeContext;
                refreshContextLabel();
            }
        }, true);

        box.append(grid, input, send);
        document.body.appendChild(box);
        clampToViewport(box, anchorX, anchorY);
        input.focus();
    }

    function showMenu(anchorX, anchorY, text) {
        removeMenu();
        scrollBaseY = window.scrollY;
        activeSelectionText = text || getSelectionText();
        if (!activeSelectionText) return;

        const menu = document.createElement("div");
        menu.id = menuId;
        menu.setAttribute("role", "menu");
        menu.setAttribute("aria-label", "Select AI tool");
        Object.assign(menu.style, {
            position: "fixed", zIndex: "9999", left: `${anchorX}px`, top: `${anchorY}px`,
            backgroundColor: "#111", border: "1px solid #333", borderRadius: "8px",
            padding: "4px", boxShadow: "0 4px 16px rgba(0,0,0,0.5)",
            fontFamily: "system-ui, sans-serif", fontSize: "12px", minWidth: "150px", userSelect: "none"
        });

        menu.addEventListener("click", (e) => e.stopPropagation());
        blockMiddleClick(menu);

        AIs.forEach((ai, i) => {
            const item = document.createElement("div");
            item.textContent = `${i + 1}. ${ai.name}${ai.copy ? " 📋" : ""}`;
            item.setAttribute("role", "menuitem");
            item.setAttribute("tabindex", "0");
            item.setAttribute("aria-label", `Open ${ai.name}`);
            Object.assign(item.style, {
                padding: "5px 10px", borderRadius: "5px", color: "#eee",
                cursor: "pointer", transition: "background 0.1s", whiteSpace: "nowrap"
            });
            item.addEventListener("mouseenter", () => item.style.backgroundColor = "#222");
            item.addEventListener("mouseleave", () => item.style.backgroundColor = "transparent");

            item.addEventListener("click", (e) => {
                e.stopPropagation();
                if (menuFiring) return;
                menuFiring = true;
                const currentText = getSelectionText() || activeSelectionText;
                if (currentText) openAI(ai, currentText);
                removeMenu();
                hideButton();
                setTimeout(() => { menuFiring = false; }, 500);
            });

            item.addEventListener("auxclick", (e) => {
                if (e.button !== 1) return;
                e.preventDefault();
                e.stopPropagation();
                const r = item.getBoundingClientRect();
                showPrompt(ai, activeSelectionText, r.right + 6, r.top);
            });

            menu.appendChild(item);
        });

        document.body.appendChild(menu);
        clampToViewport(menu, anchorX, anchorY);
    }

    function createButton() {
        let btn = document.getElementById(buttonId);
        if (!btn) {
            btn = document.createElement("button");
            btn.id = buttonId;
            btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
            btn.title = "Ask AI (Alt+Q, Shift+1-7 for prompt)";
            btn.setAttribute("aria-label", "Ask AI");
            Object.assign(btn.style, {
                position: "fixed", zIndex: "9999", width: "32px", height: "32px",
                border: "none", borderRadius: "50%", backgroundColor: "#111",
                cursor: "pointer", outline: "none", display: "none",
                alignItems: "center", justifyContent: "center",
                boxShadow: "0 2px 8px rgba(0,0,0,0.5)", transition: "background 0.15s"
            });
            btn.addEventListener("mouseenter", () => btn.style.backgroundColor = "#333");
            btn.addEventListener("mouseleave", () => btn.style.backgroundColor = "#111");
            btn.addEventListener("click", (e) => {
                e.stopPropagation();
                if (document.getElementById(menuId)) { removeMenu(); return; }
                const r = btn.getBoundingClientRect();
                showMenu(r.right + 6, r.top);
            });
            blockMiddleClick(btn);
            document.body.appendChild(btn);
        }

        const text = getSelectionText();
        if (text) {
            const bounds = getSelectionBounds();
            const posX = bounds ? bounds.x + 6 : mouseX + 12;
            const posY = bounds ? bounds.y - 6 : mouseY + 12;
            btn.style.left = `${Math.max(8, Math.min(posX, window.innerWidth - 40))}px`;
            btn.style.top = `${Math.max(8, Math.min(posY, window.innerHeight - 40))}px`;
            btn.style.display = "flex";
        } else {
            btn.style.display = "none";
            removeMenu();
        }
    }

    document.addEventListener("mousemove", trackMouse);
    document.addEventListener("keydown", handleKeyDown);
    document.addEventListener("click", handleClick);
    window.addEventListener("scroll", handleScroll, { passive: true });
    document.addEventListener("selectionchange", handleSelectionChange);
    window.addEventListener("load", createButton);
    window.addEventListener("beforeunload", cleanup);
})();