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);
})();