Fast Search

Quickly search various sites using custom shortcuts with an improved UI.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Fast Search
// @namespace    fast-search
// @version      0.1.6
// @description  Quickly search various sites using custom shortcuts with an improved UI.
// @author       JJJ
// @icon         https://th.bing.com/th/id/OUG.FC606EBD21BF6D1E0D5ABF01EACD594E?rs=1&pid=ImgDetMain
// @match        *://*/*
// @exclude      https://www.youtube.com/*/videos
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        window.focus
// @run-at       document-end
// @require      https://unpkg.com/react@17/umd/react.production.min.js
// @require      https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ===================================================
    // CONFIGURATION
    // ===================================================
    const SEARCH_ENGINES = {
        // Search
        a: { name: "Amazon", url: "https://www.amazon.com/s?k=" },
        g: { name: "Google", url: "https://www.google.com/search?q=" },
        b: { name: "Bing", url: "https://www.bing.com/search?q=" },
        d: { name: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
        gs: { name: "Google Scholar", url: "https://scholar.google.com/scholar?q=" },
        gi: { name: "Google Images", url: "https://www.google.com/search?tbm=isch&q=" },
        ar: { name: "Internet Archive", url: "https://archive.org/search.php?query=" },
        way: { name: "Wayback Machine", url: "https://web.archive.org/web/*/" },
        w: { name: "Wikipedia", url: "https://en.wikipedia.org/w/index.php?search=" },
        p: { name: "Perplexity", url: "https://www.perplexity.ai/?q=" },

        // Coding
        gf: { name: "Greasy Fork", url: "https://greasyfork.org/en/scripts?q=" },
        gh: { name: "GitHub", url: "https://github.com/search?q=" },
        so: { name: "Stack Overflow", url: "https://stackoverflow.com/search?q=" },

        // Social
        r: { name: "Reddit", url: "https://www.reddit.com/search/?q=" },
        li: { name: "LinkedIn", url: "https://www.linkedin.com/search/results/all/?keywords=" },
        t: { name: "Twitch", url: "https://www.twitch.tv/search?term=" },
        x: { name: "Twitter", url: "https://twitter.com/search?q=" },
        f: { name: "Facebook", url: "https://www.facebook.com/search/top/?q=" },
        i: { name: "Instagram", url: "https://www.instagram.com/explore/tags/" },
        pi: { name: "Pinterest", url: "https://www.pinterest.com/search/pins/?q=" },
        tu: { name: "Tumblr", url: "https://www.tumblr.com/search/" },
        q: { name: "Quora", url: "https://www.quora.com/search?q=" },
        sc: { name: "SoundCloud", url: "https://soundcloud.com/search?q=" },
        y: { name: "YouTube", url: "https://www.youtube.com/results?search_query=" },
        tk: { name: "TikTok", url: "https://www.tiktok.com/search?q=" },
        fi: { name: "Find That Meme", url: "https://findthatmeme.com/?search=" },
        sp: { name: "Spotify", url: "https://open.spotify.com/search/" },

        // Gaming
        steam: { name: "Steam", url: "https://store.steampowered.com/search/?term=" },
        epic: { name: "Epic Games", url: "https://store.epicgames.com/en-US/browse?q=" },
        gog: { name: "GOG", url: "https://www.gog.com/games?search=" },
        ubi: { name: "Ubisoft", url: "https://store.ubi.com/us/search?q=" },
        g2: { name: "G2A", url: "https://www.g2a.com/search?query=" },
        cd: { name: "CDKeys", url: "https://www.cdkeys.com/catalogsearch/result/?q=" },
        ori: { name: "Origin", url: "https://www.origin.com/search?searchString=" },
        bat: { name: "Battle.net", url: "https://shop.battle.net/search?q=" },

        // Movies and TV Shows
        c: { name: "Cuevana", url: "https://wow.cuevana3.nu/search?s=" },
        lm: { name: "LookMovie (Movies)", url: "https://www.lookmovie2.to/movies/search/?q=" },
        ls: { name: "LookMovie (Shows)", url: "https://www.lookmovie2.to/shows/search/?q=" },
    };

    // ===================================================
    // UTILITY FUNCTIONS
    // ===================================================
    const Utils = {
        /**
         * Check if the focus is in an editable element
         * @returns {boolean} True if focus is in editable element
         */
        isFocusInEditable: () => {
            const el = document.activeElement;
            return el.isContentEditable || ['input', 'textarea'].includes(el.tagName.toLowerCase());
        },

        /**
         * Construct search URL from shortcut and query
         * @param {string} shortcut - The search engine shortcut
         * @param {string} query - The search query
         * @returns {string} The constructed search URL
         */
        constructSearchUrl: (shortcut, query) => {
            const engine = SEARCH_ENGINES[shortcut] || SEARCH_ENGINES.g;
            if (!query.trim()) {
                // Extract base domain using regex
                const match = engine.url.match(/^https?:\/\/([\w.-]+\.[a-z]{2,})/);
                return match ? `https://${match[1]}/` : engine.url;
            }
            let baseUrl = engine.url;
            if (shortcut === 'epic') {
                baseUrl += `${encodeURIComponent(query)}&sortBy=relevancy&sortDir=DESC&count=40`;
            } else {
                baseUrl += encodeURIComponent(query);
            }
            return baseUrl;
        },

        /**
         * Get selected text from the page
         * @returns {string} The currently selected text
         */
        getSelectedText: () => {
            return window.getSelection().toString().trim();
        },

        /**
         * Filter search engines based on input
         * @param {string} input - The user input
         * @returns {Array} Array of matching engine options
         */
        filterSearchEngines: (input) => {
            if (!input) return [];
            const searchTerm = input.toLowerCase();
            return Object.entries(SEARCH_ENGINES)
                .filter(([shortcut, engine]) => {
                    return shortcut.toLowerCase().includes(searchTerm) ||
                        engine.name.toLowerCase().includes(searchTerm);
                })
                .slice(0, 6) // Limit to 6 suggestions
                .map(([shortcut, engine]) => ({
                    shortcut,
                    name: engine.name
                }));
        },

        /**
         * Safely remove event listeners
         * @param {Element} element - DOM element
         * @param {string} eventType - Event type
         * @param {Function} handler - Event handler
         */
        safeRemoveEventListener: (element, eventType, handler) => {
            if (element && typeof element.removeEventListener === 'function') {
                element.removeEventListener(eventType, handler);
            }
        }
    };

    // ===================================================
    // SEARCH FUNCTIONS
    // ===================================================
    const SearchActions = {
        /**
         * Open search URL based on openMode setting
         * @param {string} url - The URL to open
         * @param {string} openMode - The mode to open the URL ('currenttab' or 'newwindow')
         */
        openSearch: (url, openMode) => {
            if (openMode === 'currenttab') {
                window.location.href = url;
            } else {
                window.open(url, '', 'width=800,height=600,noopener');
            }
        },

        /**
         * Search multiple gaming platforms
         * @param {string} query - The search query
         * @param {string} openMode - The mode to open the URLs
         */
        searchMultipleGamingPlatforms: (query, openMode) => {
            const platforms = ['g2', 'cd'];
            platforms.forEach(platform => {
                const searchUrl = Utils.constructSearchUrl(platform, query);
                SearchActions.openSearch(searchUrl, openMode);
            });
        }
    };

    // ===================================================
    // REACT COMPONENTS
    // ===================================================

    /**
     * EngineSuggestions Component - Display search engine suggestions
     */
    const EngineSuggestions = React.memo(({
        suggestions,
        selectedIndex,
        onSelectSuggestion
    }) => {
        if (!suggestions || suggestions.length === 0) return null;

        return React.createElement('div', {
            className: 'absolute left-0 right-0 top-full mt-1 bg-custom-darker rounded-md shadow-lg z-10 max-h-64 overflow-y-auto'
        },
            React.createElement('ul', { className: 'py-1' },
                suggestions.map((suggestion, index) =>
                    React.createElement('li', {
                        key: suggestion.shortcut,
                        className: `px-3 py-2 cursor-pointer hover:bg-blue-600 text-white ${index === selectedIndex ? 'bg-blue-600' : ''}`,
                        onClick: () => onSelectSuggestion(suggestion.shortcut)
                    },
                        React.createElement('span', { className: 'inline-block min-w-[40px] font-mono text-blue-400' }, suggestion.shortcut),
                        ': ',
                        suggestion.name
                    )
                )
            )
        );
    });

    /**
     * SearchInput Component - Handles user input for search with keyboard navigation
     */
    const SearchInput = React.memo(({
        input,
        setInput,
        handleSearch,
        currentEngine,
        engineOptions = []
    }) => {
        const inputRef = React.useRef(null);
        const [showSuggestions, setShowSuggestions] = React.useState(false);
        const [selectedIndex, setSelectedIndex] = React.useState(-1);
        const [suggestions, setSuggestions] = React.useState([]);

        // Generate engine suggestions based on input
        React.useEffect(() => {
            const engineSuggestions = Utils.filterSearchEngines(input);
            setSuggestions(engineSuggestions);
            // Reset selection when suggestions change
            setSelectedIndex(-1);

            // Cleanup unnecessary references
            return () => {
                setSuggestions([]);
            };
        }, [input]);

        React.useEffect(() => {
            if (inputRef.current) {
                inputRef.current.focus();
            }

            // Cleanup function to help garbage collection
            return () => {
                inputRef.current = null;
            };
        }, []);

        const handleKeyDown = React.useCallback((e) => {
            // Handle arrow navigation for engine suggestions
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                setShowSuggestions(true);
                setSelectedIndex(prev =>
                    prev < suggestions.length - 1 ? prev + 1 : 0
                );
            }
            else if (e.key === 'ArrowUp') {
                e.preventDefault();
                setShowSuggestions(true);
                setSelectedIndex(prev =>
                    prev > 0 ? prev - 1 : suggestions.length - 1
                );
            }
            else if (e.key === 'Tab') {
                // Cycle through common engine shortcuts with Tab
                e.preventDefault();
                const commonShortcuts = ['g', 'y', 'w', 'r', 'a'];
                const currentShortcut = input.split(' ')[0];
                const currentIndex = commonShortcuts.indexOf(currentShortcut);
                const nextShortcut = commonShortcuts[(currentIndex + 1) % commonShortcuts.length];

                // Replace the current shortcut or add a new one
                if (currentIndex >= 0) {
                    const rest = input.substring(currentShortcut.length);
                    setInput(nextShortcut + rest);
                } else {
                    setInput(nextShortcut + ' ' + input);
                }
            }
            else if (e.key === 'Enter') {
                // Apply selected suggestion or perform search
                if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
                    const selectedShortcut = suggestions[selectedIndex].shortcut;
                    setInput(selectedShortcut + ' ');
                    setSelectedIndex(-1);
                    setShowSuggestions(false);
                } else {
                    handleSearch();
                }
            }
            else if (e.key === 'Escape') {
                // Close suggestions panel
                setShowSuggestions(false);
                setSelectedIndex(-1);
            }
            // Reset selection when typing regular characters
            else if (e.key.length === 1) {
                setShowSuggestions(true);
            }
        }, [input, suggestions, selectedIndex, setInput, handleSearch]);

        const handleSelectSuggestion = React.useCallback((shortcut) => {
            setInput(shortcut + ' ');
            setShowSuggestions(false);
            setSelectedIndex(-1);
            setTimeout(() => inputRef.current?.focus(), 10);
        }, [setInput]);

        return React.createElement('div', { className: 'flex flex-col mb-4 relative' },
            React.createElement('div', { className: 'flex gap-2 items-center' },
                currentEngine && React.createElement('div', {
                    className: 'bg-blue-600 text-white text-sm px-2 py-1 rounded'
                }, currentEngine.name),
                React.createElement('input', {
                    ref: inputRef,
                    type: 'text',
                    value: input,
                    onChange: (e) => setInput(e.target.value),
                    onKeyDown: handleKeyDown,
                    onFocus: () => setShowSuggestions(true),
                    onBlur: () => setTimeout(() => setShowSuggestions(false), 200),
                    placeholder: 'Enter search command...',
                    className: 'flex-1 px-3 py-2 bg-custom-darker border-0 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500'
                })
            ),
            showSuggestions && suggestions.length > 0 && React.createElement(EngineSuggestions, {
                suggestions,
                selectedIndex,
                onSelectSuggestion: handleSelectSuggestion
            })
        );
    });

    /**
     * ModeSwitcher Component - Toggles between current tab and new window modes
     */
    const ModeSwitcher = React.memo(({ openMode, setOpenMode }) => {
        const toggleMode = React.useCallback(() => {
            setOpenMode(openMode === 'currenttab' ? 'newwindow' : 'currenttab');
        }, [openMode, setOpenMode]);

        return React.createElement('div', { className: 'mb-4 flex items-center justify-between' },
            React.createElement('div', { className: 'flex items-center gap-3' },
                React.createElement('button', {
                    onClick: toggleMode,
                    className: 'toggle-button-switch flex items-center justify-start'
                },
                    React.createElement('div', {
                        className: `toggle-slider ${openMode === 'currenttab' ? 'active' : ''}`
                    })
                ),
                React.createElement('span', { className: 'text-gray-300 text-sm leading-none' },
                    openMode === 'newwindow' ? 'New Window' : 'Current Tab'
                )
            )
        );
    });

    /**
     * SearchResults Component - Displays search results
     */
    const SearchResults = React.memo(({ results }) => {
        return React.createElement('div', { className: 'space-y-2' },
            results.map((result, index) =>
                React.createElement('div', { key: index, className: 'text-sm' },
                    result.type === 'link'
                        ? React.createElement('a', {
                            href: result.url,
                            target: '_blank',
                            rel: 'noopener noreferrer',
                            className: 'text-blue-400 hover:text-blue-300 hover:underline'
                        }, result.message)
                        : React.createElement('span', {
                            className: 'text-gray-300'
                        }, result.message)
                )
            )
        );
    });

    /**
     * HelpContent Component - Shows the help modal content
     */
    const HelpContent = React.memo(({ onClose }) => {
        return React.createElement('div', {
            className: 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[2147483647]',
            onClick: onClose
        },
            React.createElement('div', {
                className: 'bg-custom-dark p-6 rounded-lg max-w-4xl max-h-[80vh] overflow-y-auto text-white w-full mx-4',
                onClick: e => e.stopPropagation()
            },
                React.createElement('div', { className: 'flex justify-between items-center mb-4' },
                    React.createElement('h3', { className: 'text-lg font-bold' }, 'Fast Search Help'),
                    React.createElement('button', {
                        onClick: onClose,
                        className: 'text-gray-400 hover:text-white text-xl'
                    }, '×')
                ),
                React.createElement('div', { className: 'grid grid-cols-2 gap-6' },
                    // Left column - Shortcuts
                    React.createElement('div', null,
                        React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Search Shortcuts'),
                        Object.entries({
                            'Search': ['a', 'g', 'b', 'd', 'gs', 'gi', 'ar', 'way', 'w', 'p'],
                            'Coding': ['gf', 'gh', 'so'],
                            'Social': ['r', 'li', 't', 'x', 'f', 'i', 'pi', 'tu', 'q', 'sc', 'y', 'tk', 'fi', 'sp'],
                            'Gaming': ['steam', 'epic', 'gog', 'ubi', 'g2', 'cd', 'ori', 'bat'],
                            'Movies and TV Shows': ['c', 'lm', 'ls']
                        }).map(([category, shortcuts]) =>
                            React.createElement('div', { key: category, className: 'mb-4' },
                                React.createElement('h5', { className: 'text-gray-300 font-bold mb-2 text-sm' }, category),
                                React.createElement('ul', { className: 'space-y-1' },
                                    shortcuts.map(shortcut =>
                                        React.createElement('li', { key: shortcut, className: 'text-sm' },
                                            React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, shortcut),
                                            ': ',
                                            SEARCH_ENGINES[shortcut].name
                                        )
                                    )
                                )
                            )
                        )
                    ),
                    // Right column - Usage & Options
                    React.createElement('div', null,
                        React.createElement('div', { className: 'mb-6' },
                            React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Opening Options'),
                            React.createElement('div', { className: 'bg-custom-darker p-4 rounded-lg' },
                                React.createElement('ul', { className: 'space-y-3' },
                                    React.createElement('li', { className: 'text-sm' },
                                        React.createElement('span', { className: 'text-blue-400 font-bold' }, 'New Window: '),
                                        'Opens search in a popup window'
                                    ),
                                    React.createElement('li', { className: 'text-sm' },
                                        React.createElement('span', { className: 'text-blue-400 font-bold' }, 'Current Tab: '),
                                        'Replaces current page with search'
                                    )
                                )
                            )
                        ),
                        React.createElement('div', { className: 'mb-6' },
                            React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Usage Tips'),
                            React.createElement('ul', { className: 'space-y-2 text-sm' },
                                React.createElement('li', null, '• Press ',
                                    React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Insert'),
                                    ' to open Fast Search'
                                ),
                                React.createElement('li', null, '• Type shortcut followed by search terms'),
                                React.createElement('li', null, '• Press ',
                                    React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Enter'),
                                    ' to search'
                                ),
                                React.createElement('li', null, '• Press ',
                                    React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Esc'),
                                    ' to close'
                                ),
                                React.createElement('li', null, '• Type shortcut only to visit site homepage')
                            )
                        )
                    )
                )
            )
        );
    });

    /**
     * BotInterface Component - Main component for the search interface
     */
    const BotInterface = React.memo(({ onClose, initialQuery = '' }) => {
        const [input, setInput] = React.useState(initialQuery);
        const [results, setResults] = React.useState([]);
        const [currentEngine, setCurrentEngine] = React.useState(null);
        const [openMode, setOpenMode] = React.useState(() => {
            return GM_getValue('fastsearch_openmode', 'newwindow');
        });
        const [showHelp, setShowHelp] = React.useState(false);

        // Track previous active element to restore focus when unmounting
        const previousActiveElement = React.useRef(document.activeElement);

        // Create a list of engine shortcuts for keyboard navigation
        const engineOptions = React.useMemo(() => {
            return Object.keys(SEARCH_ENGINES);
        }, []);

        // Save openMode changes
        React.useEffect(() => {
            GM_setValue('fastsearch_openmode', openMode);
        }, [openMode]);

        // Handle escape key to close
        React.useEffect(() => {
            const handleEscape = (e) => {
                if (e.key === 'Escape') {
                    onClose();
                }
            };

            document.addEventListener('keydown', handleEscape);
            return () => {
                Utils.safeRemoveEventListener(document, 'keydown', handleEscape);

                // Restore focus to previous element when unmounting
                if (previousActiveElement.current) {
                    try {
                        previousActiveElement.current.focus();
                    } catch (e) {
                        // Ignore focus errors
                    }
                }
            };
        }, [onClose]);

        // Update current engine based on input
        React.useEffect(() => {
            const [shortcut] = input.trim().split(/\s+/);
            const engine = SEARCH_ENGINES[shortcut.toLowerCase()];
            setCurrentEngine(engine || null);

            // Clear references on unmount
            return () => {
                setCurrentEngine(null);
            };
        }, [input]);

        // Memoized search handler
        const handleSearch = React.useCallback(() => {
            const [rawShortcut, ...queryParts] = input.trim().split(/\s+/);
            const shortcut = rawShortcut.toLowerCase();
            const query = queryParts.join(" ");

            let newResults = [];

            if (shortcut === 'sg') {
                newResults.push({ type: 'info', message: 'Searching multiple gaming platforms...' });
                SearchActions.searchMultipleGamingPlatforms(query, openMode);
            } else if (SEARCH_ENGINES.hasOwnProperty(shortcut)) {
                const searchUrl = Utils.constructSearchUrl(shortcut, query || '');
                const siteName = SEARCH_ENGINES[shortcut].name;
                newResults.push({ type: 'link', url: searchUrl, message: `Searching ${siteName} for "${query}"` });
                SearchActions.openSearch(searchUrl, openMode);
            } else {
                const searchUrl = SEARCH_ENGINES.g.url + encodeURIComponent(input);
                newResults.push({ type: 'link', url: searchUrl, message: `Searching Google for "${input}"` });
                SearchActions.openSearch(searchUrl, openMode);
            }

            setResults(prevResults => [...newResults, ...prevResults]);
            setInput('');

            // Close the UI after performing the search
            setTimeout(() => {
                onClose();
            }, 100);
        }, [input, openMode, onClose]);

        // Toggle help dialog
        const toggleHelp = React.useCallback(() => {
            setShowHelp(prev => !prev);
        }, []);

        return React.createElement('div', { className: 'fixed top-4 right-4 min-w-[20rem] max-w-[30rem] w-[90vw] bg-custom-dark shadow-lg rounded-lg overflow-hidden' },
            React.createElement('div', { className: 'p-4 relative' },
                // Header
                React.createElement('div', { className: 'flex justify-between items-center mb-4' },
                    React.createElement('h2', { className: 'text-lg font-bold text-white' }, 'Fast Search'),
                    React.createElement('button', {
                        onClick: onClose,
                        className: 'text-gray-400 hover:text-gray-200'
                    }, '×')
                ),
                // Search input
                React.createElement(SearchInput, {
                    input,
                    setInput,
                    handleSearch,
                    currentEngine,
                    engineOptions
                }),
                // Mode switcher and help button
                React.createElement('div', { className: 'mb-4 flex items-center justify-between' },
                    React.createElement(ModeSwitcher, {
                        openMode,
                        setOpenMode
                    }),
                    React.createElement('button', {
                        onClick: toggleHelp,
                        className: 'bg-custom-darker text-white px-3 py-1 rounded hover:bg-blue-600 transition-colors'
                    }, '❔')
                ),
                // Search results
                React.createElement(SearchResults, { results }),
                // Help modal
                showHelp && React.createElement(HelpContent, { onClose: toggleHelp })
            )
        );
    });

    // ===================================================
    // MAIN APP INITIALIZATION
    // ===================================================
    const App = {
        botContainer: null,
        observer: null,
        eventListeners: [],

        /**
         * Register event listener with automatic cleanup
         * @param {Element} element - DOM element
         * @param {string} eventType - Event type
         * @param {Function} handler - Event handler
         * @param {boolean|object} options - Event listener options
         */
        registerEventListener: (element, eventType, handler, options = false) => {
            if (!element || !eventType || !handler) return;

            element.addEventListener(eventType, handler, options);
            App.eventListeners.push({ element, eventType, handler, options });
        },

        /**
         * Clean up resources to prevent memory leaks
         */
        cleanup: () => {
            // Clean up React components properly
            if (App.botContainer) {
                ReactDOM.unmountComponentAtNode(App.botContainer);
                App.botContainer.remove();
                App.botContainer = null;
            }

            // Disconnect mutation observer if it exists
            if (App.observer) {
                App.observer.disconnect();
                App.observer = null;
            }

            // Remove all registered event listeners
            App.eventListeners.forEach(({ element, eventType, handler, options }) => {
                Utils.safeRemoveEventListener(element, eventType, handler, options);
            });
            App.eventListeners = [];
        },

        /**
         * Show the search interface with optional initial query
         * @param {string} initialQuery - Text to prefill in search input
         */
        showBot: (initialQuery = '') => {
            // Clean up any existing instances first to prevent duplicates
            App.cleanup();

            App.botContainer = document.createElement('div');
            document.body.appendChild(App.botContainer);

            ReactDOM.render(
                React.createElement(BotInterface, {
                    onClose: () => {
                        App.cleanup();
                    },
                    initialQuery: initialQuery
                }),
                App.botContainer
            );

            // Set up mutation observer to detect if our container gets removed
            App.observer = new MutationObserver((mutations) => {
                if (!document.body.contains(App.botContainer) && App.botContainer !== null) {
                    App.cleanup();
                }
            });

            // Watch for changes to document.body
            App.observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        },

        /**
         * Initialize the application
         */
        init: () => {
            // Event listener for Insert key
            App.registerEventListener(document, 'keydown', event => {
                if (event.key === 'Insert' && !Utils.isFocusInEditable()) {
                    event.preventDefault();

                    // Use selected text as initial query if available
                    const selectedText = Utils.getSelectedText();
                    App.showBot(selectedText);
                }
            }, true);

            // Register context menu command
            GM_registerMenuCommand("Fast Search", () => {
                const selectedText = Utils.getSelectedText();
                App.showBot(selectedText);
            });

            // Add context menu functionality for right-clicking on selected text
            App.registerEventListener(document, 'mousedown', event => {
                // Only handle right-click events
                if (event.button === 2) {
                    const selectedText = Utils.getSelectedText();
                    if (selectedText) {
                        // Store the selected text so we can use it later if the context menu command is chosen
                        GM_setValue('fastsearch_selected_text', selectedText);
                    }
                }
            });

            // Cleanup on page unload
            App.registerEventListener(window, 'beforeunload', App.cleanup);

            // Cleanup on page visibility change (helps with some browsers/scenarios)
            App.registerEventListener(document, 'visibilitychange', () => {
                if (document.visibilityState === 'hidden') {
                    // Perform partial cleanup when page is hidden
                    if (App.botContainer) {
                        ReactDOM.unmountComponentAtNode(App.botContainer);
                    }
                }
            });
        }
    };

    // Add styles
    GM_addStyle(`
        .fixed { position: fixed; }
        .top-4 { top: 1rem; }
        .right-4 { right: 1rem; }
        .w-80 { width: 20rem; }
        .bg-custom-dark { background-color: #030d22; }
        .bg-custom-darker { background-color: #15132a; }
        .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); }
        .rounded-lg { border-radius: 0.5rem; }
        .overflow-hidden { overflow: hidden; }
        /* Add very high z-index to ensure it's above everything */
        .fixed.top-4.right-4 { z-index: 2147483647; }
        .p-4 { padding: 1rem; }
        .flex { display: flex; }
        .justify-between { justify-content: space-between; }
        .items-center { align-items: center; }
        .mb-4 { margin-bottom: 1rem; }
        .text-lg { font-size: 1.125rem; }
        .font-bold { font-weight: 700; }
        .text-white { color: white; }
        .text-gray-200 { color: #e5e7eb; }
        .text-gray-300 { color: #d1d5db; }
        .text-gray-400 { color: #9ca3af; }
        .hover\\:text-gray-200:hover { color: #e5e7eb; }
        .w-full { width: 100%; }
        .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
        .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
        .rounded-l-md { border-top-left-radius: 0.375rem; border-bottom-left-radius: 0.375rem; }
        .focus\\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
        .focus\\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
        .focus\\:ring-blue-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgba(59, 130, 246, var(--tw-ring-opacity)); }
        .px-4 { padding-left: 1rem; padding-right: 1rem; }
        .bg-blue-600 { background-color: #2563eb; }
        .hover\\:bg-blue-700:hover { background-color: #1d4ed8; }
        .text-blue-400 { color: #60a5fa; }
        .hover\\:text-blue-300:hover { color: #93c5fd; }
        .rounded-r-md { border-top-right-radius: 0.375rem; border-bottom-right-radius: 0.375rem; }
        .space-y-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); }
        .text-sm { font-size: 0.875rem; }
        .hover\\:underline:hover { text-decoration: underline; }
        .placeholder-gray-400::placeholder { color: #9ca3af; }
        .relative { position: relative; }
        .transform { transform: var(--tw-transform); }
        .-translate-y-1/2 { --tw-translate-y: -50%; transform: var(--tw-transform); }
        .text-xs { font-size: 0.75rem; line-height: 1rem; }
        .pl-24 { padding-left: 6rem; }
        .top-1/2 { top: 50%; }
        .left-2 { left: 0.5rem; }
        .min-w-\\[20rem\\] { min-width: 20rem; }
        .max-w-\\[30rem\\] { max-width: 30rem; }
        .w-\\[90vw\\] { width: 90vw; }
        .gap-2 { gap: 0.5rem; }
        .flex-1 { flex: 1 1 0%; }
        .flex-shrink-0 { flex-shrink: 0; }
        .whitespace-nowrap { white-space: nowrap; }
        .rounded-md { border-radius: 0.375rem; }
        .min-w-\\[80px\\] { min-width: 80px; }
        .-translate-y-6 { --tw-translate-y: -1.5rem; }
        .toggle-switch {
            position: relative;
            display: inline-block;
            width: 50px;
            height: 24px;
        }
        .toggle-checkbox {
            opacity: 0;
            width: 0;
            height: 0;
        }
        .toggle-label {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #15132a;
            transition: .4s;
            border-radius: 24px;
        }
        .toggle-button {
            position: absolute;
            height: 20px;
            width: 20px;
            left: 2px;
            bottom: 2px;
            background-color: #2563eb;
            transition: .4s;
            border-radius: 50%;
        }
        .toggle-checkbox:checked + .toggle-label .toggle-button {
            transform: translateX(26px);
        }
        .grid { display: grid; }
        .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
        .gap-6 { gap: 1.5rem; }
        .bg-custom-darker { background-color: #15132a; }
        .p-6 { padding: 1.5rem; }
        .max-w-4xl { max-width: 56rem; }
        .max-h-\\[80vh\\] { max-height: 80vh; }
        .overflow-y-auto { overflow-y: auto; }
        .space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
        .toggle-button-switch {
            position: relative;
            width: 50px;
            height: 24px;
            background-color: #15132a;
            border-radius: 24px;
            padding: 2px;
            border: none;
            cursor: pointer;
            outline: none;
            display: flex;
            align-items: center;
        }
        .toggle-slider {
            position: absolute;
            height: 20px;
            width: 20px;
            background-color: #2563eb;
            border-radius: 50%;
            transition: transform 0.3s;
        }
        .toggle-slider.active {
            transform: translateX(26px);
        }
        .gap-3 {
            gap: 0.75rem;
        }
        .leading-none {
            line-height: 1;
        }
        .hover\\:bg-blue-600:hover {
            background-color: #2563eb;
        }
        .transition-colors {
            transition-property: color, background-color, border-color;
            transition-duration: 0.15s;
            transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
        }
        
        /* New styles for engine suggestions */
        .flex-col {
            flex-direction: column;
        }
        
        .top-full {
            top: 100%;
        }
        
        .mt-1 {
            margin-top: 0.25rem;
        }
        
        .py-1 {
            padding-top: 0.25rem;
            padding-bottom: 0.25rem;
        }
        
        .max-h-64 {
            max-height: 16rem;
        }
        
        .overflow-y-auto {
            overflow-y: auto;
        }
        
        .cursor-pointer {
            cursor: pointer;
        }
        
        .font-mono {
            font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
        }
        
        .min-w-\\[40px\\] {
            min-width: 40px;
        }
        
        .inline-block {
            display: inline-block;
        }
        
        .z-10 {
            z-index: 10;
        }
    `);

    // Start the app
    App.init();
})();