├── LICENSE ├── README.md ├── background.js ├── content-chatgpt.js ├── content-claude.js ├── icons ├── ChatSurfer-128.png ├── ChatSurfer-16.png ├── ChatSurfer-48.png └── ChatSurferScreenshot.jpg ├── manifest.json ├── popup.html ├── popup.js └── video └── videoplayback.mp4 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kevin Roosey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat Surfers 2 | ## LLMs take forever to load responses. Watch some fun gameplay while you wait 3 | 4 | ![Screenshot of the App](./icons/ChatSurferScreenshot.jpg) 5 | 6 | ### Update: Chatsurfers now runs on the following sites: 7 | - [ChatGPT](https://www.chatgpt.com) 8 | - [Claude](https://www.claude.ai) 9 | 10 | #### Feel free to fork to add another site. I'll most likely merge if you open a PR 11 | 12 | ## Quick Start 13 | 14 | 1. **Clone the repository** 15 | 2. **Head to [chrome://extensions](chrome://extensions)** 16 | 3. **Click Load Unpacked** 17 | 4. **Select the chatsurfers repository** 18 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | console.log("Background script running..."); 2 | 3 | chrome.runtime.onInstalled.addListener(() => { 4 | console.log("Extension installed!"); 5 | }); 6 | 7 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 8 | if (request.enabled === false) { 9 | chrome.storage.local.set({ extensionEnabled: false }); 10 | console.log("Extension disabled globally."); 11 | } 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /content-chatgpt.js: -------------------------------------------------------------------------------- 1 | console.log("ChatGPT extension content script loaded."); 2 | 3 | // Global observer variable 4 | let chatGPTObserver = null; 5 | let isExtensionEnabled = false; // Track extension state 6 | 7 | // Request storage state from the background script 8 | chrome.storage.local.get('extensionEnabled', function (data) { 9 | isExtensionEnabled = data.extensionEnabled || false; 10 | if (isExtensionEnabled) { 11 | activateChatsurfers(); 12 | } 13 | }); 14 | 15 | // Listen for messages from `popup.js` 16 | chrome.runtime.onMessage.addListener(function (request) { 17 | isExtensionEnabled = request.enabled; 18 | if (request.enabled) { 19 | activateChatsurfers(); 20 | } else { 21 | disableChatsurfers(); 22 | } 23 | }); 24 | 25 | function createPopup() { 26 | // Don't create popup if extension is disabled 27 | if (!isExtensionEnabled) return; 28 | 29 | let existingPopup = document.getElementById("chatgpt-popup"); 30 | if (!existingPopup) { 31 | let popup = document.createElement("div"); 32 | popup.id = "chatgpt-popup"; 33 | popup.style.position = "fixed"; 34 | popup.style.bottom = "20px"; 35 | popup.style.right = "20px"; 36 | popup.style.backgroundColor = "black"; 37 | popup.style.padding = "10px"; 38 | popup.style.borderRadius = "5px"; 39 | popup.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)"; 40 | popup.style.zIndex = "10000"; 41 | popup.style.display = "flex"; 42 | popup.style.flexDirection = "column"; 43 | 44 | let contentContainer = document.createElement("div"); 45 | contentContainer.style.position = "relative"; 46 | contentContainer.style.width = "100%"; 47 | 48 | let video = document.createElement("video"); 49 | video.id = "chatgpt-video"; 50 | video.src = chrome.runtime.getURL("video/videoplayback.mp4"); 51 | video.width = 400; 52 | video.height = 250; 53 | video.muted = true; 54 | video.autoplay = true; 55 | video.loop = true; 56 | 57 | let disableBtn = document.createElement("button"); 58 | disableBtn.innerText = "Disable"; 59 | disableBtn.style.position = "absolute"; 60 | disableBtn.style.top = "10px"; 61 | disableBtn.style.right = "10px"; 62 | disableBtn.style.padding = "8px 16px"; 63 | disableBtn.style.fontSize = "14px"; 64 | disableBtn.style.border = "none"; 65 | disableBtn.style.borderRadius = "5px"; 66 | disableBtn.style.backgroundColor = "red"; 67 | disableBtn.style.color = "white"; 68 | disableBtn.style.cursor = "pointer"; 69 | disableBtn.style.zIndex = "1"; 70 | 71 | disableBtn.addEventListener("mouseover", () => { 72 | disableBtn.style.backgroundColor = "#ff4444"; 73 | }); 74 | disableBtn.addEventListener("mouseout", () => { 75 | disableBtn.style.backgroundColor = "red"; 76 | }); 77 | 78 | disableBtn.addEventListener("click", function (e) { 79 | e.preventDefault(); 80 | e.stopPropagation(); 81 | 82 | isExtensionEnabled = false; 83 | chrome.storage.local.set({ extensionEnabled: false }, function () { 84 | console.log("Extension disabled from content script."); 85 | chrome.runtime.sendMessage({ enabled: false }); 86 | disableChatsurfers(); 87 | }); 88 | }); 89 | 90 | contentContainer.appendChild(video); 91 | contentContainer.appendChild(disableBtn); 92 | popup.appendChild(contentContainer); 93 | document.body.appendChild(popup); 94 | 95 | video.play().catch(error => console.log("Autoplay prevented:", error)); 96 | } 97 | } 98 | 99 | function removePopup() { 100 | let popup = document.getElementById("chatgpt-popup"); 101 | if (popup) { 102 | // Stop video playback 103 | let video = popup.querySelector('video'); 104 | if (video) { 105 | video.pause(); 106 | video.src = ""; 107 | video.load(); 108 | } 109 | popup.remove(); 110 | console.log("Popup removed successfully"); 111 | } 112 | } 113 | 114 | function isChatGPTResponding() { 115 | if (!isExtensionEnabled) return false; 116 | const stopButton = document.querySelector('button[data-testid="stop-button"]'); 117 | const sendButton = document.querySelector('button[data-testid="send-button"]'); 118 | return (stopButton !== null) || (sendButton && sendButton.disabled); 119 | } 120 | 121 | function observeChatGPTButton() { 122 | if (!isExtensionEnabled) return; 123 | 124 | const chatContainer = document.body; 125 | if (!chatContainer) { 126 | console.warn("ChatGPT container not found. Retrying..."); 127 | setTimeout(observeChatGPTButton, 1000); 128 | return; 129 | } 130 | 131 | if (chatGPTObserver) { 132 | chatGPTObserver.disconnect(); 133 | } 134 | 135 | chatGPTObserver = new MutationObserver(() => { 136 | if (isExtensionEnabled && isChatGPTResponding()) { 137 | console.log("ChatGPT is responding..."); 138 | createPopup(); 139 | } else { 140 | console.log("ChatGPT response complete or extension disabled."); 141 | removePopup(); 142 | } 143 | }); 144 | 145 | chatGPTObserver.observe(chatContainer, { childList: true, subtree: true }); 146 | console.log("ChatGPT observer started."); 147 | } 148 | 149 | function activateChatsurfers() { 150 | console.log("Chatsurfers Enabled"); 151 | isExtensionEnabled = true; 152 | observeChatGPTButton(); 153 | } 154 | 155 | function disableChatsurfers() { 156 | console.log("Chatsurfers Disabled"); 157 | isExtensionEnabled = false; 158 | 159 | if (chatGPTObserver) { 160 | chatGPTObserver.disconnect(); 161 | chatGPTObserver = null; 162 | } 163 | 164 | removePopup(); 165 | } 166 | 167 | // Only start if extension is enabled 168 | if (isExtensionEnabled) { 169 | observeChatGPTButton(); 170 | } -------------------------------------------------------------------------------- /content-claude.js: -------------------------------------------------------------------------------- 1 | console.log("Claude extension content script loaded."); 2 | 3 | // Global observer variable 4 | let claudeObserver = null; 5 | let isExtensionEnabled = false; // Track extension state 6 | 7 | // Request storage state from the background script 8 | chrome.storage.local.get('extensionEnabled', function (data) { 9 | isExtensionEnabled = data.extensionEnabled || false; 10 | if (isExtensionEnabled) { 11 | activateChatsurfers(); 12 | } 13 | }); 14 | 15 | // Listen for messages from `popup.js` 16 | chrome.runtime.onMessage.addListener(function (request) { 17 | isExtensionEnabled = request.enabled; 18 | if (request.enabled) { 19 | activateChatsurfers(); 20 | } else { 21 | disableChatsurfers(); 22 | } 23 | }); 24 | 25 | function createPopup() { 26 | // Don't create popup if extension is disabled 27 | if (!isExtensionEnabled) return; 28 | 29 | let existingPopup = document.getElementById("chatgpt-popup"); 30 | if (!existingPopup) { 31 | let popup = document.createElement("div"); 32 | popup.id = "chatgpt-popup"; 33 | popup.style.position = "fixed"; 34 | popup.style.bottom = "20px"; 35 | popup.style.right = "20px"; 36 | popup.style.backgroundColor = "black"; 37 | popup.style.padding = "10px"; 38 | popup.style.borderRadius = "5px"; 39 | popup.style.boxShadow = "0 2px 10px rgba(0,0,0,0.3)"; 40 | popup.style.zIndex = "10000"; 41 | popup.style.display = "flex"; 42 | popup.style.flexDirection = "column"; 43 | 44 | let contentContainer = document.createElement("div"); 45 | contentContainer.style.position = "relative"; 46 | contentContainer.style.width = "100%"; 47 | 48 | let video = document.createElement("video"); 49 | video.id = "chatgpt-video"; 50 | video.src = chrome.runtime.getURL("video/videoplayback.mp4"); 51 | video.width = 400; 52 | video.height = 250; 53 | video.muted = true; 54 | video.autoplay = true; 55 | video.loop = true; 56 | 57 | let disableBtn = document.createElement("button"); 58 | disableBtn.innerText = "Disable"; 59 | disableBtn.style.position = "absolute"; 60 | disableBtn.style.top = "10px"; 61 | disableBtn.style.right = "10px"; 62 | disableBtn.style.padding = "8px 16px"; 63 | disableBtn.style.fontSize = "14px"; 64 | disableBtn.style.border = "none"; 65 | disableBtn.style.borderRadius = "5px"; 66 | disableBtn.style.backgroundColor = "red"; 67 | disableBtn.style.color = "white"; 68 | disableBtn.style.cursor = "pointer"; 69 | disableBtn.style.zIndex = "1"; 70 | 71 | disableBtn.addEventListener("mouseover", () => { 72 | disableBtn.style.backgroundColor = "#ff4444"; 73 | }); 74 | disableBtn.addEventListener("mouseout", () => { 75 | disableBtn.style.backgroundColor = "red"; 76 | }); 77 | 78 | disableBtn.addEventListener("click", function (e) { 79 | e.preventDefault(); 80 | e.stopPropagation(); 81 | 82 | isExtensionEnabled = false; 83 | chrome.storage.local.set({ extensionEnabled: false }, function () { 84 | console.log("Extension disabled from content script."); 85 | chrome.runtime.sendMessage({ enabled: false }); 86 | disableChatsurfers(); 87 | }); 88 | }); 89 | 90 | contentContainer.appendChild(video); 91 | contentContainer.appendChild(disableBtn); 92 | popup.appendChild(contentContainer); 93 | document.body.appendChild(popup); 94 | 95 | video.play().catch(error => console.log("Autoplay prevented:", error)); 96 | } 97 | } 98 | 99 | function removePopup() { 100 | let popup = document.getElementById("chatgpt-popup"); 101 | if (popup) { 102 | // Stop video playback 103 | let video = popup.querySelector('video'); 104 | if (video) { 105 | video.pause(); 106 | video.src = ""; 107 | video.load(); 108 | } 109 | popup.remove(); 110 | console.log("Popup removed successfully"); 111 | } 112 | } 113 | 114 | // Claude functionality 115 | function isClaudeResponding() { 116 | if (!isExtensionEnabled) return false; 117 | const stopButton = document.querySelector('button[aria-label="Stop Response"]'); 118 | const sendButton = document.querySelector('button[aria-label="Send Message"]'); 119 | return (stopButton !== null) || (sendButton && sendButton.disabled); 120 | } 121 | 122 | function observeClaudeButton() { 123 | if (!isExtensionEnabled) return; 124 | 125 | const chatContainer = document.body; 126 | if (!chatContainer) { 127 | console.warn("Claude container not found. Retrying..."); 128 | setTimeout(observeClaudeButton, 1000); 129 | return; 130 | } 131 | 132 | if (claudeObserver) { 133 | claudeObserver.disconnect(); 134 | } 135 | 136 | claudeObserver = new MutationObserver(() => { 137 | if (isExtensionEnabled && isClaudeResponding()) { 138 | console.log("ChatGPT is responding..."); 139 | createPopup(); 140 | } else { 141 | console.log("ChatGPT response complete or extension disabled."); 142 | removePopup(); 143 | } 144 | }); 145 | 146 | claudeObserver.observe(chatContainer, { childList: true, subtree: true }); 147 | console.log("ChatGPT observer started."); 148 | } 149 | 150 | function activateChatsurfers() { 151 | console.log("Chatsurfers Enabled"); 152 | isExtensionEnabled = true; 153 | observeClaudeButton(); 154 | } 155 | 156 | function disableChatsurfers() { 157 | console.log("Chatsurfers Disabled"); 158 | isExtensionEnabled = false; 159 | 160 | if (claudeObserver) { 161 | claudeObserver.disconnect(); 162 | claudeObserver = null; 163 | } 164 | 165 | removePopup(); 166 | } 167 | 168 | // Only start if extension is enabled 169 | if (isExtensionEnabled) { 170 | observeClaudeButton(); 171 | } 172 | -------------------------------------------------------------------------------- /icons/ChatSurfer-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsk3vin/chatsurfers/3201f1cb2dbe18e45f462dbfc7f93860e92b15d1/icons/ChatSurfer-128.png -------------------------------------------------------------------------------- /icons/ChatSurfer-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsk3vin/chatsurfers/3201f1cb2dbe18e45f462dbfc7f93860e92b15d1/icons/ChatSurfer-16.png -------------------------------------------------------------------------------- /icons/ChatSurfer-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsk3vin/chatsurfers/3201f1cb2dbe18e45f462dbfc7f93860e92b15d1/icons/ChatSurfer-48.png -------------------------------------------------------------------------------- /icons/ChatSurferScreenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsk3vin/chatsurfers/3201f1cb2dbe18e45f462dbfc7f93860e92b15d1/icons/ChatSurferScreenshot.jpg -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Chatsurfers", 4 | "version": "1.0", 5 | "description": "Some entertainment while you wait for o1 to respond", 6 | "permissions": [ 7 | "scripting", 8 | "activeTab", 9 | "storage" 10 | ], 11 | "host_permissions": [ 12 | "https://chatgpt.com/*", 13 | "https://claude.ai/*" 14 | ], 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "https://chatgpt.com/*" 22 | ], 23 | "js": [ 24 | "content-chatgpt.js" 25 | ], 26 | "run_at": "document_idle" 27 | }, 28 | { 29 | "matches": ["*://claude.ai/*"], 30 | "js": ["content-claude.js"] 31 | 32 | } 33 | ], 34 | "web_accessible_resources": [ 35 | { 36 | "resources": [ 37 | "video/videoplayback.mp4" 38 | ], 39 | "matches": [ 40 | "" 41 | ] 42 | } 43 | ], 44 | "icons": { 45 | "16": "icons/ChatSurfer-16.png", 46 | "48": "icons/ChatSurfer-48.png", 47 | "128": "icons/ChatSurfer-128.png" 48 | }, 49 | "action": { 50 | "default_popup": "popup.html", 51 | "default_icon": { 52 | "16": "icons/ChatSurfer-16.png", 53 | "48": "icons/ChatSurfer-48.png", 54 | "128": "icons/ChatSurfer-128.png" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup 8 | 9 | 10 | 104 | 105 | 106 | 107 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | const enableBtn = document.getElementById('enable-btn'); 3 | const disableBtn = document.getElementById('disable-btn'); 4 | 5 | // Get initial state 6 | chrome.storage.local.get('extensionEnabled', function (data) { 7 | const isEnabled = data.extensionEnabled || false; 8 | updateButtonStates(isEnabled); 9 | }); 10 | 11 | enableBtn.addEventListener('click', function () { 12 | chrome.storage.local.set({ extensionEnabled: true }, function () { 13 | // Send message to content script 14 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 15 | chrome.tabs.sendMessage(tabs[0].id, { enabled: true }); 16 | }); 17 | updateButtonStates(true); 18 | }); 19 | }); 20 | 21 | disableBtn.addEventListener('click', function () { 22 | chrome.storage.local.set({ extensionEnabled: false }, function () { 23 | // Send message to content script 24 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 25 | chrome.tabs.sendMessage(tabs[0].id, { enabled: false }); 26 | }); 27 | updateButtonStates(false); 28 | }); 29 | }); 30 | 31 | function updateButtonStates(isEnabled) { 32 | enableBtn.disabled = isEnabled; 33 | disableBtn.disabled = !isEnabled; 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /video/videoplayback.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsk3vin/chatsurfers/3201f1cb2dbe18e45f462dbfc7f93860e92b15d1/video/videoplayback.mp4 --------------------------------------------------------------------------------