ChatGPT Conversation Navigator

Displays a floating container on the right with every message you sent in the current conversation. Clicking a message scrolls smoothly to it.

目前为 2025-03-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ChatGPT Conversation Navigator
  3. // @namespace https://greasyfork.org/en/users/1444872-tlbstation
  4. // @author TLBSTATION
  5. // @icon https://i.ibb.co/jZ3HpwPk/pngwing-com.png
  6. // @version 1.4.2
  7. // @description Displays a floating container on the right with every message you sent in the current conversation. Clicking a message scrolls smoothly to it.
  8. // @match https://chatgpt.com/*
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let chatID = ''; // To track conversation changes
  17. let userMsgCounter = 0;
  18.  
  19. // Create (or retrieve) the floating container on the right
  20. function createContainer() {
  21. let container = document.getElementById('chatgpt-message-nav');
  22. if (!container) {
  23. container = document.createElement('div');
  24. container.id = 'chatgpt-message-nav';
  25. container.style.position = 'fixed';
  26. container.style.top = '60px';
  27. container.style.right = '10px';
  28. container.style.width = '250px';
  29. container.style.maxHeight = '80vh';
  30. container.style.overflowY = 'auto';
  31.  
  32.  
  33. container.className = "text-token-text-primary bg-token-main-surface-primary rounded-lg shadow-lg";
  34. container.style.zIndex = '1';
  35. container.style.borderRadius = '8px';
  36. container.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';
  37. container.style.fontSize = '14px';
  38. container.style.transition = 'width 0.3s, padding 0.3s';
  39.  
  40. // Create header with title and toggle button
  41. const header = document.createElement('div');
  42. header.id = 'chatgpt-message-nav-header';
  43. header.style.display = 'flex';
  44. header.style.alignItems = 'center';
  45. header.style.justifyContent = 'space-between';
  46. header.style.padding = '10px';
  47. header.style.paddingTop = '15px';
  48. header.style.cursor = 'pointer';
  49. header.style.position = 'sticky';
  50. header.style.top = '-7px';
  51. header.style.background = 'inherit';
  52. header.style.zIndex = '1';
  53.  
  54.  
  55. const title = document.createElement('div');
  56. title.id = 'chatgpt-message-nav-title';
  57. title.innerText = 'Your Messages';
  58. title.style.fontWeight = 'bold';
  59.  
  60. const toggleBtn = document.createElement('button');
  61. toggleBtn.id = 'chatgpt-message-nav-toggle';
  62. toggleBtn.style.background = 'none';
  63. toggleBtn.style.border = 'none';
  64. toggleBtn.style.color = 'white';
  65. toggleBtn.style.fontSize = '16px';
  66. toggleBtn.style.cursor = 'pointer';
  67. // Default state: expanded, so show "<"
  68. toggleBtn.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md text-token-text-primary"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C11.7348 21 11.4804 20.8946 11.2929 20.7071L4.29289 13.7071C3.90237 13.3166 3.90237 12.6834 4.29289 12.2929C4.68342 11.9024 5.31658 11.9024 5.70711 12.2929L11 17.5858V4C11 3.44772 11.4477 3 12 3C12.5523 3 13 3.44772 13 4V17.5858L18.2929 12.2929C18.6834 11.9024 19.3166 11.9024 19.7071 12.2929C20.0976 12.6834 20.0976 13.3166 19.7071 13.7071L12.7071 20.7071C12.5196 20.8946 12.2652 21 12 21Z" fill="currentColor"></path></svg>`;
  69. toggleBtn.style.rotate = '-90deg';
  70. toggleBtn.style.transition = 'rotate 0.3s';
  71. header.appendChild(title);
  72. header.appendChild(toggleBtn);
  73.  
  74. // Create content container for the list
  75. const content = document.createElement('div');
  76. content.id = 'chatgpt-message-nav-content';
  77. content.style.padding = '10px';
  78. content.style.paddingTop = '0px';
  79.  
  80. container.appendChild(header);
  81. container.appendChild(content);
  82.  
  83. document.body.appendChild(container);
  84.  
  85. // Check if collapsed state is stored
  86. const collapsed = localStorage.getItem('chatgptMessageNavCollapsed') === 'true';
  87. if (collapsed) {
  88. content.style.display = 'none';
  89. container.style.width = 'min-content';
  90. container.style.padding = '5px';
  91. title.style.display = 'none';
  92. toggleBtn.style.rotate = '90deg';
  93. header.style.paddingTop = '10px';
  94. }
  95.  
  96. // Toggle functionality
  97. toggleBtn.addEventListener('click', () => {
  98. if (content.style.display === 'none') {
  99. // Expand
  100. content.style.display = 'block';
  101. container.style.width = '250px';
  102. container.style.padding = '7px';
  103. title.style.display = 'block';
  104. toggleBtn.style.rotate = '-90deg';
  105. header.style.paddingTop = '15px';
  106. localStorage.setItem('chatgptMessageNavCollapsed', 'false');
  107. } else {
  108. // Collapse
  109. content.style.display = 'none';
  110. container.style.width = 'min-content';
  111. container.style.padding = '5px';
  112. title.style.display = 'none';
  113. toggleBtn.style.rotate = '90deg';
  114. header.style.paddingTop = '10px';
  115. localStorage.setItem('chatgptMessageNavCollapsed', 'true');
  116. }
  117. });
  118. }
  119. return container;
  120. }
  121.  
  122. // Ensure each user message gets a unique ID
  123. function assignIdToMessage(msgElem) {
  124. if (!msgElem.id) {
  125. userMsgCounter++;
  126. msgElem.id = 'user-msg-' + userMsgCounter;
  127. }
  128. }
  129.  
  130. // Update the list in the floating container with all user messages
  131. function updateMessageList() {
  132. const container = createContainer();
  133. const content = document.getElementById('chatgpt-message-nav-content');
  134. if (!content) return;
  135.  
  136. // Preserve current scroll position
  137. const prevScrollTop = content.scrollTop;
  138.  
  139. const list = content.querySelector('ul');
  140. if (!list) {
  141. // Create the list only once
  142. const newList = document.createElement('ul');
  143. newList.style.padding = '0';
  144. newList.style.margin = '0';
  145. newList.style.listStyle = 'none';
  146. content.appendChild(newList);
  147. }
  148.  
  149. // Get the existing message IDs to avoid duplicate entries
  150. const existingIds = new Set([...content.querySelectorAll('li')].map(li => li.dataset.msgId));
  151.  
  152. // Select user messages
  153. const userMessages = document.querySelectorAll('div[data-message-author-role="user"]');
  154. userMessages.forEach(msgElem => {
  155. assignIdToMessage(msgElem);
  156. const text = msgElem.innerText.trim();
  157.  
  158. // Skip if already in the list
  159. if (existingIds.has(msgElem.id)) return;
  160.  
  161. const listItem = document.createElement('li');
  162. listItem.dataset.msgId = msgElem.id; // Store msg ID to prevent duplication
  163. listItem.style.cursor = 'pointer';
  164. listItem.style.padding = '5px 10px';
  165. listItem.style.marginTop = '5px';
  166. listItem.style.borderRadius = '10px';
  167. listItem.style.borderBottom = '1px solid var(--main-surface-primary-inverse)';
  168. listItem.style.transition = 'background 0.2s';
  169. listItem.style.whiteSpace = 'nowrap';
  170. listItem.style.overflow = 'hidden';
  171. listItem.style.textOverflow = 'ellipsis';
  172.  
  173. listItem.addEventListener('mouseenter', () => {
  174. listItem.style.background = '#c5c5c54d' ;
  175. });
  176. listItem.addEventListener('mouseleave', () => {
  177. listItem.style.background = 'transparent';
  178. });
  179.  
  180. listItem.textContent = text;
  181.  
  182. // Smooth scroll to message when clicked
  183. listItem.addEventListener('click', () => {
  184. const target = document.getElementById(msgElem.id);
  185. if (target) {
  186. target.scrollIntoView({ behavior: 'smooth', block: 'start' });
  187. }
  188. });
  189.  
  190. // Append new list item
  191. content.querySelector('ul').appendChild(listItem);
  192. });
  193.  
  194. // Restore scroll position after updating the list
  195. content.scrollTop = prevScrollTop;
  196. }
  197.  
  198.  
  199. // Get the current conversation ID based on the URL
  200. function getChatID() {
  201. const chatURL = window.location.pathname;
  202. return chatURL.includes('/c/') ? chatURL.split('/c/')[1] : 'global';
  203. }
  204.  
  205. // Observe the conversation container for changes
  206. function observeConversation() {
  207. const mainElem = document.querySelector('main');
  208. if (!mainElem) return;
  209. const observer = new MutationObserver(() => {
  210. updateMessageList();
  211. });
  212. observer.observe(mainElem, { childList: true, subtree: true });
  213. }
  214.  
  215. function toggleContainerVisibility() {
  216. const container = document.getElementById('chatgpt-message-nav');
  217. const isChatPage = window.location.pathname.startsWith('/c/');
  218.  
  219. if (container) {
  220. container.style.display = isChatPage ? 'block' : 'none';
  221. }
  222. }
  223.  
  224.  
  225. // Wait for the conversation area to load
  226. function waitForChat() {
  227. const interval = setInterval(() => {
  228. if (document.querySelector('main')) {
  229. clearInterval(interval);
  230. toggleContainerVisibility();
  231. chatID = getChatID();
  232. updateMessageList();
  233. observeConversation();
  234. }
  235. }, 500);
  236. }
  237.  
  238. waitForChat();
  239.  
  240. // Update when switching between conversations (for SPA navigation)
  241. let lastUrl = location.href;
  242. const urlObserver = new MutationObserver(() => {
  243. if (location.href !== lastUrl) {
  244. lastUrl = location.href;
  245. chatID = getChatID();
  246. userMsgCounter = 0;
  247. updateMessageList();
  248. toggleContainerVisibility();
  249. }
  250. });
  251.  
  252. urlObserver.observe(document.body, { childList: true, subtree: true });
  253. })();