Interactive Sidebar Navigator for ChatGPT

为ChatGPT官网提供了不遮挡页眉、页脚或滚动条的用户友好、交互性强的侧边栏。

  1. // ==UserScript==
  2. // @name Interactive Sidebar Navigator for ChatGPT
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0
  5. // @description A user-friendly, interactive sidebar for the ChatGPT official website that does not cover the header, footer, or the scrollbar.
  6. // @description:zh-CN 为ChatGPT官网提供了不遮挡页眉、页脚或滚动条的用户友好、交互性强的侧边栏。
  7. // @license GPL-3.0-or-later
  8. // @match https://chat.openai.com/**
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Define the header and footer heights if known, or estimate
  17. const headerHeight = '60px'; // Change this value to the actual height of your header
  18. const footerHeight = '60px'; // Change this value to the actual height of your footer
  19.  
  20. // Insert custom styles into the document
  21. const style = document.createElement('style');
  22. style.type = 'text/css';
  23. style.innerHTML = `
  24. #customSidebar {
  25. position: fixed;
  26. top: ${headerHeight};
  27. bottom: ${footerHeight};
  28. right: 0;
  29. width: 250px;
  30. background-color: #000;
  31. color: #fff;
  32. overflow-y: auto;
  33. overflow-x: hidden;
  34. transition: transform 0.3s ease-out;
  35. transform: translateX(250px);
  36. z-index: 9999;
  37. box-shadow: -2px 0 5px rgba(0,0,0,0.5);
  38. padding-bottom: 10px; // Adjusted padding for footer
  39. }
  40.  
  41. #customSidebar div {
  42. padding: 10px;
  43. border-bottom: 1px solid #444;
  44. text-overflow: ellipsis;
  45. overflow: hidden;
  46. white-space: nowrap;
  47. cursor: pointer;
  48. }
  49.  
  50. #customSidebar div.active {
  51. background-color: #777; // Highlight active item
  52. }
  53.  
  54. /* Tooltip styles */
  55. .sidebar-tooltip {
  56. visibility: hidden;
  57. background-color: #555;
  58. color: #fff;
  59. text-align: left;
  60. border-radius: 5px;
  61. padding: 5px;
  62. position: absolute;
  63. z-index: 10001;
  64. left: 100%;
  65. top: 0;
  66. white-space: pre-wrap;
  67. word-wrap: break-word;
  68. opacity: 0;
  69. transition: visibility 0s, opacity 0.5s linear;
  70. }
  71.  
  72. .tooltip {
  73. visibility: hidden;
  74. background-color: #555;
  75. color: #fff;
  76. text-align: left;
  77. border-radius: 5px;
  78. padding: 5px;
  79. position: absolute;
  80. z-index: 10001;
  81. left: 100%;
  82. margin-left: 10px; // Space between item and tooltip
  83. white-space: nowrap;
  84. word-wrap: break-word;
  85. transition: visibility 0s linear 0.3s, opacity 0.3s linear 0.3s;
  86. opacity: 0;
  87. pointer-events: none; // Tooltip should not interfere with mouse events
  88. }
  89.  
  90. .sidebar-item:hover .tooltip {
  91. visibility: visible;
  92. opacity: 1;
  93. transition-delay: 0s;
  94. }
  95.  
  96. .sidebar-tooltip-show {
  97. visibility: visible;
  98. opacity: 1;
  99. transition-delay: 3s; // Delay to show tooltip after 3s of hover
  100. }
  101. .sidebar-item:hover::after {
  102. visibility: visible;
  103. opacity: 1;
  104. transition-delay: 0s;
  105. }
  106.  
  107. #tooltipContainer {
  108. display: none; // Start with the tooltip container not displayed
  109. position: fixed;
  110. z-index: 10001;
  111. pointer-events: none; // Ensure the tooltip does not interfere with mouse events
  112. transition: opacity 0.3s ease-in-out;
  113. opacity: 0;
  114. }
  115.  
  116. .visible #tooltipContainer {
  117. display: block; // Display tooltip container when a tooltip is visible
  118. opacity: 1;
  119. }
  120.  
  121. #sidebarToggle {
  122. position: fixed;
  123. right: 250px;
  124. top: calc(50% - 20px); // Center toggle vertically, adjusting for its own height
  125. transform: translateX(100%);
  126. z-index: 10000;
  127. cursor: pointer;
  128. background-color: #444;
  129. color: #fff;
  130. border: none;
  131. width: 30px;
  132. height: 40px;
  133. border-radius: 5px 0 0 5px;
  134. outline: none;
  135. transition: right 0.3s ease-out, transform 0.3s ease-out;
  136. }
  137.  
  138. .sidebar-icon-bar {
  139. display: block;
  140. width: 20px;
  141. height: 2px;
  142. background-color: #fff;
  143. margin: 6px auto;
  144. transition: background-color 0.3s, transform 0.3s ease-out;
  145. }
  146.  
  147. .toggle-open .top-bar {
  148. transform: translateY(8px) rotateZ(45deg);
  149. }
  150.  
  151. .toggle-open .middle-bar {
  152. opacity: 0;
  153. }
  154.  
  155. .toggle-open .bottom-bar {
  156. transform: translateY(-8px) rotateZ(-45deg);
  157. }
  158.  
  159. body {
  160. padding-right: 250px; // Make space for the sidebar when it is expanded
  161. }
  162. div.sticky.top-0 {
  163. opacity: 0.3;
  164. }
  165. `;
  166.  
  167. document.head.appendChild(style);
  168.  
  169. let questionAnswerSelector = '.flex-col.gap-1.md\\:gap-3'
  170. let customSidebarSelector = '#customSidebar > div'
  171. let questionAnswerLength=2
  172. let customSidebarLlength=0
  173.  
  174. function updateSidebarContent(){
  175. let elementWithAttribute = document.querySelector(questionAnswerSelector);
  176.  
  177. if (elementWithAttribute) {
  178. sidebar.innerHTML = ''
  179. console.log('elementWithAttribute exists: update')
  180. }else{
  181. console.log('elementWithAttribute not exists')
  182. // updateSidebarContent()
  183. }
  184. const allTextItems = Array.from(document.querySelectorAll(questionAnswerSelector));
  185. if (allTextItems.length > 0) {
  186. // observer.disconnect();
  187.  
  188. // Populate the sidebar with items
  189. allTextItems.forEach((item, index) => {
  190. if (index % 2 === 0) { // Add odd items
  191. const div = document.createElement('div');
  192. div.textContent = item.textContent.trim() || 'Untitled';
  193. // createTooltip(div, div.textContent); // Add tooltip to each item
  194. div.setAttribute('title', div.textContent); // Set the title for default browser tooltip
  195.  
  196.  
  197.  
  198. // Setup mouse events for showing and hiding the tooltip
  199. let hoverTimeout;
  200. div.addEventListener('mouseenter', (e) => {
  201. const rect = div.getBoundingClientRect();
  202. hoverTimeout = setTimeout(() => {
  203. showTooltip(div.textContent, rect.right, rect.top + window.scrollY);
  204. }, 3000);
  205. });
  206. div.addEventListener('mouseleave', () => {
  207. clearTimeout(hoverTimeout);
  208. hideTooltip();
  209. });
  210.  
  211. div.addEventListener('click', () => {
  212. // Scroll to the element on the page
  213. item.scrollIntoView({ behavior: 'smooth', block: 'start' });
  214. // Highlight the active item
  215. document.querySelectorAll('#customSidebar div').forEach(d => d.classList.remove('active'));
  216. div.classList.add('active');
  217. });
  218. sidebar.appendChild(div);
  219. }
  220.  
  221. // Adjust sidebar overflow after populating it
  222. adjustSidebarOverflow();
  223. });
  224.  
  225. // Initially open the sidebar
  226. sidebar.style.transform = 'translateX(0)';
  227. toggleButton.classList.add('toggle-open');
  228. // Adjust the toggle button position
  229. setToggleButtonPosition();
  230.  
  231.  
  232. const customSidebar = document.getElementById('customSidebar');
  233. if (customSidebar) {
  234. const divs = customSidebar.querySelectorAll('div');
  235. divs.forEach((div, index) => {
  236. div.textContent = `${index + 1}: ${div.textContent}`;
  237. });
  238. } else {
  239. console.log('#customSidebar not found');
  240. }
  241.  
  242. }
  243. }
  244.  
  245.  
  246. // Create a global tooltip container
  247. const tooltipContainer = document.createElement('div');
  248. tooltipContainer.id = 'tooltipContainer';
  249. document.body.appendChild(tooltipContainer);
  250.  
  251.  
  252. // Function to update tooltip content and position
  253. function showTooltip(text, x, y) {
  254. tooltipContainer.textContent = text;
  255. tooltipContainer.style.top = `${y}px`;
  256. tooltipContainer.style.left = `${x}px`;
  257. tooltipContainer.classList.add('visible');
  258. }
  259.  
  260. function hideTooltip() {
  261. tooltipContainer.classList.remove('visible');
  262. }
  263.  
  264. // Create the sidebar element
  265. const sidebar = document.createElement('div');
  266. sidebar.id = 'customSidebar';
  267. document.body.appendChild(sidebar);
  268.  
  269. // Create the toggle button
  270. const toggleButton = document.createElement('button');
  271. toggleButton.id = 'sidebarToggle';
  272. toggleButton.innerHTML = `
  273. <div class="sidebar-icon-bar top-bar"></div>
  274. <div class="sidebar-icon-bar middle-bar"></div>
  275. <div class="sidebar-icon-bar bottom-bar"></div>
  276. `;
  277. document.body.appendChild(toggleButton);
  278.  
  279. // Function to set the correct position of the toggle button
  280. function setToggleButtonPosition() {
  281. const isSidebarVisible = sidebar.style.transform === 'translateX(0px)';
  282. toggleButton.style.right = isSidebarVisible ? '250px' : '0';
  283. toggleButton.style.transform = `translateX(${isSidebarVisible ? '-100%' : '0'})`;
  284. }
  285.  
  286. // Initial call to set the toggle button position
  287. setToggleButtonPosition();
  288.  
  289. // Toggle functionality
  290. toggleButton.addEventListener('click', function() {
  291. const isClosed = sidebar.style.transform.includes('250px');
  292. sidebar.style.transform = isClosed ? 'translateX(0)' : 'translateX(250px)';
  293. toggleButton.classList.toggle('toggle-open', isClosed);
  294. // Wait for the transition to finish before adjusting the toggle button position
  295. setTimeout(setToggleButtonPosition, 300);
  296. });
  297.  
  298.  
  299. // Adjust sidebar overflow based on its content height
  300. function adjustSidebarOverflow() {
  301. const sidebar = document.getElementById('customSidebar');
  302. if (sidebar.scrollHeight > sidebar.clientHeight) {
  303. sidebar.style.overflowY = 'auto';
  304. } else {
  305. sidebar.style.overflowY = 'hidden';
  306. }
  307. }
  308.  
  309. // Observe mutations to the page content
  310. const observer = new MutationObserver(mutations => {
  311. questionAnswerLength = document.querySelectorAll(questionAnswerSelector).length/2
  312. customSidebarLlength = document.querySelectorAll(customSidebarSelector).length
  313. if (questionAnswerLength == customSidebarLlength){
  314. console.log(`questionAnswerLength: ${questionAnswerLength} == customSidebarLlength: ${customSidebarLlength}`)
  315. return
  316. }
  317. mutations.forEach(mutation => {
  318. // Check if the mutation occurs on a form element or its descendants
  319. let target = mutation.target;
  320. while (target !== document && target.nodeName !== 'FORM') {
  321. target = target.parentNode;
  322. }
  323.  
  324. // If the change does not occur on a form element, perform the corresponding action
  325. if (target.nodeName !== 'FORM') {
  326. console.log('Performing changes on non-form element');
  327. console.log('Invoke updateSidebarContent');
  328. sidebar.innerHTML = '';
  329. updateSidebarContent();
  330. }
  331. });
  332.  
  333.  
  334. });
  335.  
  336. // Configuration for the observer
  337. const config = {
  338. // attributes: true, // Listen for attribute changes
  339. // characterData: true, // Listen for text content changes
  340. childList: true, // Listen for additions/removals of child elements
  341. subtree: true // Listen for changes in all descendant nodes
  342. };
  343.  
  344.  
  345. let checkExist = setInterval(function() {
  346. // '.flex-col.gap-1.md\\:gap-3'
  347. // 'div.group.relative.active\\:opacity-90'
  348. // Find the element to observe (e.g., main content area)
  349. const observeElement = document.querySelector('main');
  350. if (observeElement != null) {
  351. console.log(`observeElement: ${observeElement}`);
  352. // Start observing mutations
  353. observer.observe(observeElement, config);
  354. clearInterval(checkExist);
  355.  
  356. }
  357. }, 1000);
  358.  
  359.  
  360. })();

QingJ © 2025

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