您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a draggable floating AI bubble with a comprehensive list of bots. Clicking an option opens a new browser window styled to look like a sidebar.
// ==UserScript== // @name AI Floating Bubble (Sidebar Window) // @version 1.6 // @description Adds a draggable floating AI bubble with a comprehensive list of bots. Clicking an option opens a new browser window styled to look like a sidebar. // @author Mayukhjit Chakraborty // @match *://*/* // @grant GM_addStyle // @license MIT // @namespace http://tampermonkey.net/ // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // This script should not run on the "sidebar" window itself const urlParams = new URLSearchParams(window.location.search); const isAISidebarWindow = urlParams.has('ai_sidebar_window'); if (isAISidebarWindow) { return; } // Do not run inside iframes; only in top-level browsing context if (window.top !== window.self) { return; } /** * @class AISites * Manages the names and URLs for AI sites, including login requirements. */ class AISites { static get LIST() { return [ { name: "ChatGPT", url: "https://chat.openai.com/", loginNeeded: false }, { name: "Gemini", url: "https://gemini.google.com/", loginNeeded: true }, { name: "Copilot", url: "https://copilot.microsoft.com/", loginNeeded: false }, { name: "Perplexity", url: "https://www.perplexity.ai/", loginNeeded: false }, { name: "Poe", url: "https://poe.com/", loginNeeded: true }, { name: "Grok", url: "https://x.com/i/grok", loginNeeded: false }, { name: "You.com", url: "https://you.com/chat", loginNeeded: true }, { name: "Claude", url: "https://claude.ai/", loginNeeded: true }, { name: "Qwen", url: "https://chat.qwen.ai/", loginNeeded: false }, { name: "Deepseek", url: "https://chat.deepseek.com/", loginNeeded: true }, ]; } } /** * @class BubbleConfig * Manages all numerical configuration values for the bubble and sidebar window. */ class BubbleConfig { static get BUBBLE_SIZE() { return 50; } static get MENU_GAP() { return 10; } static get MENU_TRANSITION_DURATION() { return 0.2; } static get MENU_HIDE_DELAY() { return 100; } static get SIDEBAR_WIDTH() { return 400; } static get SIDEBAR_HEIGHT_PERCENT() { return 0.9; } static get ITEM_PADDING_VERTICAL() { return 12; } static get ITEM_PADDING_HORIZONTAL() { return 20; } } /** * @class AIFloatingBubble * The main class for managing the AI floating bubble and sidebar window. */ class AIFloatingBubble { constructor() { this.bubbleContainer = null; this.bubbleButton = null; this.siteOptions = null; this.hideTimeout = null; this.isDragging = false; this.offsetX = 0; this.offsetY = 0; this.isHidden = false; this._init(); } /** * @private * Initializes the bubble and sidebar functionality. */ _init() { this._createElements(); this._applyStyles(); this._loadPosition(); this._loadHiddenState(); this._setupEventListeners(); } /** * @private * Creates and appends the necessary DOM elements. */ _createElements() { // Main bubble container this.bubbleContainer = document.createElement('div'); this.bubbleContainer.id = 'aiFloatingBubbleContainer'; document.body.appendChild(this.bubbleContainer); // Bubble button this.bubbleButton = document.createElement('div'); this.bubbleButton.id = 'aiFloatingBubbleButton'; this.bubbleButton.textContent = 'AI'; this.bubbleButton.setAttribute('role', 'button'); this.bubbleButton.setAttribute('aria-label', 'Open AI menu'); this.bubbleButton.setAttribute('aria-expanded', 'false'); this.bubbleButton.setAttribute('tabindex', '0'); this.bubbleContainer.appendChild(this.bubbleButton); // AI site options menu this.siteOptions = document.createElement('div'); this.siteOptions.id = 'aiSiteOptions'; this.siteOptions.setAttribute('role', 'menu'); this.siteOptions.setAttribute('aria-hidden', 'true'); AISites.LIST.forEach(site => { const option = document.createElement('button'); option.className = 'ai-option'; option.textContent = site.name; option.setAttribute('role', 'menuitem'); option.setAttribute('type', 'button'); // Add login needed text if applicable if (site.loginNeeded) { const loginSpan = document.createElement('span'); loginSpan.className = 'login-needed-text'; loginSpan.textContent = ' [Login]'; option.appendChild(loginSpan); } option.dataset.url = site.url; this.siteOptions.appendChild(option); }); // Divider and hide control const controlsDivider = document.createElement('div'); controlsDivider.className = 'ai-divider'; this.siteOptions.appendChild(controlsDivider); const hideButton = document.createElement('button'); hideButton.className = 'ai-option ai-option-control'; hideButton.textContent = 'Hide Bubble'; hideButton.setAttribute('role', 'menuitem'); hideButton.setAttribute('type', 'button'); hideButton.dataset.action = 'hide'; this.siteOptions.appendChild(hideButton); this.bubbleContainer.appendChild(this.siteOptions); } /** * @private * Dynamically adds required CSS styles. */ _applyStyles() { GM_addStyle(` /* Floating bubble container */ #aiFloatingBubbleContainer { position: fixed; z-index: 2147483647; transition: transform 0.2s ease-in-out; cursor: grab; } #aiFloatingBubbleContainer.grabbing { cursor: grabbing; } #aiFloatingBubbleContainer.hidden { display: none !important; } /* Bubble button */ #aiFloatingBubbleButton { width: ${BubbleConfig.BUBBLE_SIZE}px; height: ${BubbleConfig.BUBBLE_SIZE}px; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); color: white; border: none; border-radius: 50%; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25); display: flex; justify-content: center; align-items: center; font-family: 'Arial', sans-serif; font-weight: bold; font-size: 1.2rem; cursor: pointer; user-select: none; transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.5s ease; } #aiFloatingBubbleButton:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } #aiFloatingBubbleButton:focus { outline: 2px solid #2575fc; outline-offset: 2px; } /* Options menu */ #aiSiteOptions { position: absolute; bottom: ${BubbleConfig.BUBBLE_SIZE + BubbleConfig.MENU_GAP}px; right: 0; background-color: #ffffff; border-radius: 10px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); overflow: hidden; min-width: 180px; opacity: 0; transform: scale(0.9); pointer-events: none; transition: opacity ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-in-out, transform ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-in-out; transform-origin: bottom right; display: flex; flex-direction: column; z-index: 2147483646; } #aiSiteOptions.visible { opacity: 1; transform: scale(1); pointer-events: auto; } /* Menu items */ .ai-option { all: unset; display: flex; align-items: center; padding: ${BubbleConfig.ITEM_PADDING_VERTICAL}px ${BubbleConfig.ITEM_PADDING_HORIZONTAL}px; cursor: pointer; color: #333333; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; border-bottom: 1px solid #f0f0f0; transition: background-color 0.2s, color 0.2s; justify-content: space-between; text-align: left; width: 100%; box-sizing: border-box; } .ai-option:hover, .ai-option:focus { background-color: #f0f8ff; color: #2575fc; } .ai-option:last-child { border-bottom: none; } .ai-option-control { font-weight: 600; } .ai-divider { height: 1px; background: #f0f0f0; } /* Login needed text style */ .login-needed-text { font-size: 12px; color: #777; font-style: italic; margin-left: 8px; } `); } /** * @private * Sets up all event listeners. */ _setupEventListeners() { this._setupDrag(); this._setupHover(); this._setupClick(); this._setupKeyboard(); this._setupResizeHandler(); } /** * @private * Configures the bubble's drag functionality. */ _setupDrag() { this.bubbleContainer.addEventListener('mousedown', (e) => { if (e.target.closest('.ai-option')) { return; } this.isDragging = true; this.bubbleContainer.classList.add('grabbing'); this.offsetX = e.clientX - this.bubbleContainer.getBoundingClientRect().left; this.offsetY = e.clientY - this.bubbleContainer.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!this.isDragging) return; const newLeft = e.clientX - this.offsetX; const newTop = e.clientY - this.offsetY; const maxX = window.innerWidth - this.bubbleContainer.offsetWidth; const maxY = window.innerHeight - this.bubbleContainer.offsetHeight; this.bubbleContainer.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`; this.bubbleContainer.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`; this.bubbleContainer.style.right = 'auto'; this.bubbleContainer.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (this.isDragging) { this.isDragging = false; this.bubbleContainer.classList.remove('grabbing'); this._savePosition(); } }); // Touch support for mobile devices this.bubbleContainer.addEventListener('touchstart', (e) => { if (e.target.closest('.ai-option')) { return; } this.isDragging = true; const touch = e.touches[0]; this.offsetX = touch.clientX - this.bubbleContainer.getBoundingClientRect().left; this.offsetY = touch.clientY - this.bubbleContainer.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener('touchmove', (e) => { if (!this.isDragging) return; const touch = e.touches[0]; const newLeft = touch.clientX - this.offsetX; const newTop = touch.clientY - this.offsetY; const maxX = window.innerWidth - this.bubbleContainer.offsetWidth; const maxY = window.innerHeight - this.bubbleContainer.offsetHeight; this.bubbleContainer.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`; this.bubbleContainer.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`; this.bubbleContainer.style.right = 'auto'; this.bubbleContainer.style.bottom = 'auto'; }); document.addEventListener('touchend', () => { if (this.isDragging) { this.isDragging = false; this._savePosition(); } }); } /** * @private * Configures the menu show/hide functionality on hover. */ _setupHover() { this.bubbleContainer.addEventListener('mouseenter', () => { if (this.isDragging) return; clearTimeout(this.hideTimeout); this.siteOptions.classList.add('visible'); this.siteOptions.setAttribute('aria-hidden', 'false'); }); this.bubbleContainer.addEventListener('mouseleave', () => { this.hideTimeout = setTimeout(() => { this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); }, BubbleConfig.MENU_HIDE_DELAY); }); } /** * @private * Sets up the click functionality for menu options. */ _setupClick() { this.bubbleButton.addEventListener('click', (e) => { e.stopPropagation(); this.siteOptions.classList.toggle('visible'); this.siteOptions.setAttribute('aria-hidden', this.siteOptions.classList.contains('visible') ? 'false' : 'true'); this.bubbleButton.setAttribute('aria-expanded', this.siteOptions.classList.contains('visible') ? 'true' : 'false'); }); this.siteOptions.addEventListener('click', (event) => { const option = event.target.closest('.ai-option'); if (option) { if (option.dataset.action === 'hide') { this._toggleHidden(true); this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); this.bubbleButton.setAttribute('aria-expanded', 'false'); return; } const url = option.dataset.url; if (url) { this._openSidebarWindow(url); this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); this.bubbleButton.setAttribute('aria-expanded', 'false'); } } }); // Close menu when clicking outside document.addEventListener('click', (e) => { if (!this.bubbleContainer.contains(e.target) && this.siteOptions.classList.contains('visible')) { this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); this.bubbleButton.setAttribute('aria-expanded', 'false'); } }); } /** * @private * Sets up keyboard navigation support. */ _setupKeyboard() { this.bubbleButton.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.siteOptions.classList.toggle('visible'); this.siteOptions.setAttribute('aria-hidden', this.siteOptions.classList.contains('visible') ? 'false' : 'true'); this.bubbleButton.setAttribute('aria-expanded', this.siteOptions.classList.contains('visible') ? 'true' : 'false'); } }); this.siteOptions.addEventListener('keydown', (e) => { const options = Array.from(this.siteOptions.querySelectorAll('.ai-option')); const currentIndex = options.indexOf(document.activeElement); switch(e.key) { case 'Escape': this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); this.bubbleButton.focus(); this.bubbleButton.setAttribute('aria-expanded', 'false'); break; case 'ArrowDown': e.preventDefault(); const nextIndex = (currentIndex + 1) % options.length; options[nextIndex].focus(); break; case 'ArrowUp': e.preventDefault(); const prevIndex = (currentIndex - 1 + options.length) % options.length; options[prevIndex].focus(); break; case 'Enter': e.preventDefault(); if (document.activeElement.classList.contains('ai-option')) { const url = document.activeElement.dataset.url; if (url) { this._openSidebarWindow(url); this.siteOptions.classList.remove('visible'); this.siteOptions.setAttribute('aria-hidden', 'true'); this.bubbleButton.setAttribute('aria-expanded', 'false'); } } break; } }); // Global hotkey: Ctrl+Shift+A toggles hidden state document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && (e.key === 'A' || e.key === 'a')) { e.preventDefault(); this._toggleHidden(!this.isHidden); } }); } /** * @private * Keeps the bubble within viewport on window resize (debounced). */ _setupResizeHandler() { let resizeTimer = null; const clampToViewport = () => { if (!this.bubbleContainer) return; const rect = this.bubbleContainer.getBoundingClientRect(); const maxX = window.innerWidth - rect.width; const maxY = window.innerHeight - rect.height; let left = this.bubbleContainer.style.left ? parseFloat(this.bubbleContainer.style.left) : null; let top = this.bubbleContainer.style.top ? parseFloat(this.bubbleContainer.style.top) : null; if (left !== null && top !== null) { left = Math.max(0, Math.min(left, maxX)); top = Math.max(0, Math.min(top, maxY)); this.bubbleContainer.style.left = `${left}px`; this.bubbleContainer.style.top = `${top}px`; this.bubbleContainer.style.right = 'auto'; this.bubbleContainer.style.bottom = 'auto'; this._savePosition(); } }; window.addEventListener('resize', () => { if (resizeTimer) { clearTimeout(resizeTimer); } resizeTimer = setTimeout(clampToViewport, 150); }); } /** * @private * Opens a new window that is sized and positioned to look like a sidebar. * @param {string} url - The URL to open in the new window. */ _openSidebarWindow(url) { try { const screenWidth = window.screen.width; const screenHeight = window.screen.height; const sidebarWidth = Math.min(BubbleConfig.SIDEBAR_WIDTH, screenWidth - 100); const sidebarHeight = Math.min(screenHeight * BubbleConfig.SIDEBAR_HEIGHT_PERCENT, screenHeight - 100); const sidebarLeft = Math.max(0, screenWidth - sidebarWidth); const sidebarTop = Math.max(0, (screenHeight - sidebarHeight) / 2); const windowName = 'AIFloatingSidebar'; const features = `width=${sidebarWidth},height=${sidebarHeight},left=${sidebarLeft},top=${sidebarTop},menubar=no,toolbar=no,location=yes,status=no,resizable=yes,scrollbars=yes`; const urlWithParam = `${url}${url.includes('?') ? '&' : '?'}ai_sidebar_window=true`; const newWindow = window.open(urlWithParam, windowName, features); if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { console.warn('Popup blocked. Please allow popups for this site.'); // Fallback: open in current tab window.location.href = url; } } catch (error) { console.error('Error opening sidebar window:', error); // Fallback: open in current tab window.location.href = url; } } /** * @private * Saves hidden state to localStorage and toggles visibility. * @param {boolean} hidden */ _toggleHidden(hidden) { this.isHidden = !!hidden; if (this.isHidden) { this.bubbleContainer.classList.add('hidden'); } else { this.bubbleContainer.classList.remove('hidden'); } this._saveHiddenState(); } /** * @private * Persist hidden state. */ _saveHiddenState() { try { localStorage.setItem('aiFloatingBubbleHidden', JSON.stringify({ hidden: this.isHidden })); } catch (error) { console.warn('Could not save hidden state:', error); } } /** * @private * Load hidden state and apply. */ _loadHiddenState() { try { const raw = localStorage.getItem('aiFloatingBubbleHidden'); if (raw) { const parsed = JSON.parse(raw); this.isHidden = !!parsed.hidden; if (this.isHidden) { this.bubbleContainer.classList.add('hidden'); } } } catch (error) { console.warn('Could not load hidden state:', error); } } /** * @private * Saves the current position of the bubble to localStorage. */ _savePosition() { try { const position = { left: this.bubbleContainer.offsetLeft, top: this.bubbleContainer.offsetTop }; localStorage.setItem('aiFloatingBubblePosition', JSON.stringify(position)); } catch (error) { console.warn('Could not save bubble position:', error); } } /** * @private * Loads the saved bubble position from localStorage. */ _loadPosition() { try { const savedPosition = localStorage.getItem('aiFloatingBubblePosition'); if (savedPosition) { const position = JSON.parse(savedPosition); this.bubbleContainer.style.left = `${position.left}px`; this.bubbleContainer.style.top = `${position.top}px`; } else { this.bubbleContainer.style.bottom = '20px'; this.bubbleContainer.style.right = '20px'; } } catch (error) { console.warn('Could not load bubble position:', error); this.bubbleContainer.style.bottom = '20px'; this.bubbleContainer.style.right = '20px'; } } } // Wait for DOM to be fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new AIFloatingBubble(); }); } else { new AIFloatingBubble(); } })();