ChatGPT desktop notification for completed response/answer

Sends a desktop notification when the ChatGPT response/answer has finished generating. It allows for toggle button dragging, memorizes the toggle button position, prevents the toggle button from going out of window bounds, and remembers the state.

  1. // ==UserScript==
  2. // @name ChatGPT desktop notification for completed response/answer
  3. // @author NWP
  4. // @description Sends a desktop notification when the ChatGPT response/answer has finished generating. It allows for toggle button dragging, memorizes the toggle button position, prevents the toggle button from going out of window bounds, and remembers the state.
  5. // @namespace https://gf.qytechs.cn/users/877912
  6. // @version 0.3
  7. // @license MIT
  8. // @match https://chatgpt.com/*
  9. // @grant GM_notification
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. let stopButtonDetected = false;
  18. let notificationsEnabled = GM_getValue('notificationsEnabled', true);
  19.  
  20. function sendNotification(title, text) {
  21. if (notificationsEnabled) {
  22. GM_notification({
  23. title: title,
  24. text: text,
  25. timeout: 5000
  26. });
  27. }
  28. }
  29.  
  30. function checkButtonState() {
  31. const stopButton = document.querySelector('button.mb-1.me-1.flex.h-8.w-8.items-center.justify-center.rounded-full.bg-black.text-white:not([disabled]) svg.icon-lg rect');
  32. if (stopButton) {
  33. if (!stopButtonDetected) {
  34. stopButtonDetected = true;
  35. }
  36. } else if (stopButtonDetected) {
  37. sendNotification('Answer finished', 'The ChatGPT answer completed.');
  38. stopButtonDetected = false;
  39. }
  40. }
  41.  
  42. const observer = new MutationObserver((mutations) => {
  43. mutations.forEach((mutation) => {
  44. if (mutation.type === 'childList' || mutation.type === 'attributes') {
  45. checkButtonState();
  46. }
  47. });
  48. });
  49.  
  50. const config = { attributes: true, childList: true, subtree: true, attributeFilter: ['disabled', 'hidden'] };
  51.  
  52. observer.observe(document.body, config);
  53.  
  54. checkButtonState();
  55.  
  56. function toggleNotifications() {
  57. notificationsEnabled = !notificationsEnabled;
  58. GM_setValue('notificationsEnabled', notificationsEnabled);
  59. updateToggleButton();
  60. }
  61.  
  62. function updateToggleButton() {
  63. const buttonText = notificationsEnabled ? "ON" : "OFF";
  64. document.getElementById('notificationToggle').textContent = `Answer notification: ${buttonText}`;
  65. }
  66.  
  67. function updateButtonPosition() {
  68. const button = document.getElementById('notificationToggle');
  69. const viewportWidth = window.innerWidth;
  70. const viewportHeight = window.innerHeight;
  71.  
  72. const leftPercent = GM_getValue('buttonLeftPercent', 90);
  73. const topPercent = GM_getValue('buttonTopPercent', 90);
  74.  
  75. button.style.left = `${leftPercent}%`;
  76. button.style.top = `${topPercent}%`;
  77.  
  78. const buttonRect = button.getBoundingClientRect();
  79. if (buttonRect.right > viewportWidth) {
  80. button.style.left = `${viewportWidth - buttonRect.width}px`;
  81. }
  82. if (buttonRect.bottom > viewportHeight) {
  83. button.style.top = `${viewportHeight - buttonRect.height}px`;
  84. }
  85. }
  86.  
  87. function createToggleButton() {
  88. const button = document.createElement('div');
  89. button.id = 'notificationToggle';
  90. button.style.position = 'fixed';
  91. button.style.width = '11.875em';
  92. button.style.height = '2.5em';
  93. button.style.padding = '0.5em';
  94. button.style.backgroundColor = '#444';
  95. button.style.color = '#fff';
  96. button.style.borderRadius = '0.5em';
  97. button.style.cursor = 'pointer';
  98. button.style.zIndex = 10000;
  99. button.style.userSelect = 'none';
  100. button.style.MozUserSelect = 'none';
  101. button.style.WebkitUserSelect = 'none';
  102. button.textContent = `Answer notification: ${notificationsEnabled ? "ON" : "OFF"}`;
  103. button.addEventListener('click', toggleNotifications);
  104.  
  105. let isDragging = false;
  106. let offsetX, offsetY;
  107.  
  108. button.addEventListener('mousedown', (e) => {
  109. isDragging = true;
  110. offsetX = e.clientX - button.getBoundingClientRect().left;
  111. offsetY = e.clientY - button.getBoundingClientRect().top;
  112. button.style.cursor = 'move';
  113. });
  114.  
  115. document.addEventListener('mousemove', (e) => {
  116. if (isDragging) {
  117. const newLeft = e.clientX - offsetX;
  118. const newTop = e.clientY - offsetY;
  119.  
  120. const buttonWidth = button.offsetWidth;
  121. const buttonHeight = button.offsetHeight;
  122. const viewportWidth = window.innerWidth;
  123. const viewportHeight = window.innerHeight;
  124.  
  125. if (newLeft < 0) {
  126. button.style.left = '0px';
  127. } else if (newLeft + buttonWidth > viewportWidth) {
  128. button.style.left = `${viewportWidth - buttonWidth}px`;
  129. } else {
  130. button.style.left = `${newLeft}px`;
  131. }
  132.  
  133. if (newTop < 0) {
  134. button.style.top = '0px';
  135. } else if (newTop + buttonHeight > viewportHeight) {
  136. button.style.top = `${viewportHeight - buttonHeight}px`;
  137. } else {
  138. button.style.top = `${newTop}px`;
  139. }
  140. }
  141. });
  142.  
  143. document.addEventListener('mouseup', () => {
  144. if (isDragging) {
  145. isDragging = false;
  146. button.style.cursor = 'pointer';
  147.  
  148. const viewportWidth = window.innerWidth;
  149. const viewportHeight = window.innerHeight;
  150.  
  151. const leftPercent = (parseInt(button.style.left) / viewportWidth) * 100;
  152. const topPercent = (parseInt(button.style.top) / viewportHeight) * 100;
  153.  
  154. GM_setValue('buttonLeftPercent', leftPercent);
  155. GM_setValue('buttonTopPercent', topPercent);
  156. }
  157. });
  158.  
  159. window.addEventListener('resize', updateButtonPosition);
  160.  
  161. document.body.appendChild(button);
  162. updateButtonPosition();
  163. }
  164.  
  165. createToggleButton();
  166. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址