ChatGPT Message Navigator

Displays a list of user and assistant responses in ChatGPT conversations for quick navigation.

  1. // ==UserScript==
  2. // @name ChatGPT Message Navigator
  3. // @namespace https://violentmonkey.github.io/
  4. // @version 1.1
  5. // @description Displays a list of user and assistant responses in ChatGPT conversations for quick navigation.
  6. // @author Bui Quoc Dung
  7. // @match https://chatgpt.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. // If panel already exists, do nothing
  16. if (document.getElementById("toc-panel") || document.getElementById("toc-handle")) {
  17. return;
  18. }
  19.  
  20. // --- Insert CSS with dark mode support ---
  21. const css = document.createElement("style");
  22. css.textContent =
  23. /* Panel */
  24. `#toc-panel { position: fixed; top: 0; right: 0; width: 280px; height: 100%; background: #fafafa; box-shadow: -4px 0 8px rgba(0,0,0,0.1); font-family: sans-serif; font-size: 0.8rem; border-left: 1px solid #ddd; display: flex; flex-direction: column; z-index: 9998; visibility: hidden; }
  25. #toc-panel.visible { visibility: visible; }
  26. #toc-header { padding: 6px 10px; background: #ddd; border-bottom: 1px solid #ccc; font-weight: bold; flex-shrink: 0; }
  27. #toc-list { list-style: none; flex: 1; overflow-y: auto; margin: 0; padding: 6px; }
  28. #toc-list li { padding: 4px; cursor: pointer; border-radius: 3px; transition: background-color 0.2s; }
  29. #toc-list li:hover { background: #f0f0f0; }
  30. #toc-handle { position: fixed; top: 50%; right: 0; transform: translateY(-50%); width: 30px; height: 80px; background: #ccc; display: flex; align-items: center; justify-content: center; writing-mode: vertical-rl; text-orientation: mixed; cursor: pointer; font-weight: bold; user-select: none; z-index: 9999; transition: background 0.2s; }
  31. #toc-handle:hover { background: #bbb; }
  32. @keyframes highlightFade { 0% { background-color: #fffa99; } 100% { background-color: transparent; } }
  33. .toc-highlight { animation: highlightFade 1.5s forwards; }
  34. @media (prefers-color-scheme: dark) {
  35. #toc-panel { background: #333; border-left: 1px solid #555; box-shadow: -4px 0 8px rgba(0,0,0,0.7); }
  36. #toc-header { background: #555; border-bottom: 1px solid #666; color: #eee; }
  37. #toc-list li:hover { background: #444; }
  38. #toc-list { color: #eee; }
  39. #toc-handle { background: #555; color: #ddd; }
  40. #toc-handle:hover { background: #666; }
  41. }`;
  42. document.head.appendChild(css);
  43.  
  44. // --- Create panel & handle ---
  45. const panel = document.createElement("div");
  46. panel.id = "toc-panel";
  47. panel.innerHTML = `
  48. <div id="toc-header">Conversation TOC</div>
  49. <ul id="toc-list"></ul>
  50. `;
  51. document.body.appendChild(panel);
  52.  
  53. const handle = document.createElement("div");
  54. handle.id = "toc-handle";
  55. handle.textContent = "TOC";
  56. document.body.appendChild(handle);
  57.  
  58. // Observed container, observer, etc.
  59. let chatContainer = null;
  60. let observer = null;
  61. let isScheduled = false;
  62. let timerId = null;
  63.  
  64. // Debounce the TOC build to avoid high CPU usage on rapid changes
  65. function debounceBuildTOC() {
  66. if (isScheduled) return;
  67. isScheduled = true;
  68. timerId = setTimeout(function () {
  69. buildTOC();
  70. isScheduled = false;
  71. }, 300);
  72. }
  73.  
  74. // Build/refresh the TOC
  75. function buildTOC() {
  76. const list = document.getElementById("toc-list");
  77. if (!list) return;
  78. list.innerHTML = "";
  79.  
  80. // Find conversation turns
  81. const articles = (chatContainer || document).querySelectorAll("article[data-testid^='conversation-turn-']");
  82. if (!articles || articles.length === 0) {
  83. list.innerHTML = '<li style="opacity:0.7;font-style:italic;">Empty chat</li>';
  84. return;
  85. }
  86.  
  87. // Loop over turns
  88. for (let i = 0; i < articles.length; i++) {
  89. const art = articles[i];
  90. const li = document.createElement("li");
  91.  
  92. // Check if AI (assistant)
  93. const sr = art.querySelector("h6.sr-only");
  94. let isAI = false;
  95. if (sr && sr.textContent.includes("ChatGPT said:")) {
  96. isAI = true;
  97. li.textContent = "ChatGPT:";
  98.  
  99. // Get the assistant message
  100. const assistantMsg = art.querySelector('div[data-message-author-role="assistant"]');
  101. const assistantText = assistantMsg?.textContent?.trim();
  102. if (assistantText) {
  103. li.innerHTML = "<strong>ChatGPT: </strong>" + assistantText.slice(0, 100) + (assistantText.length > 100 ? "..." : "");
  104. }
  105. } else {
  106. // Get the user message
  107. const userMsg = art.querySelector('div[data-message-author-role="user"]');
  108. const userText = userMsg?.textContent?.trim();
  109. if (userText) {
  110. const preview = userText.slice(0, 100);
  111. li.innerHTML = "<strong>You: </strong>" + preview + (userText.length > 100 ? "..." : "");
  112. } else {
  113. li.textContent = "";
  114. }
  115. }
  116.  
  117. // On click: scroll to turn
  118. (function (turnElem) {
  119. li.addEventListener("click", function () {
  120. turnElem.scrollIntoView({behavior: "smooth", block: "start"});
  121. });
  122. })(art);
  123.  
  124. list.appendChild(li);
  125. }
  126. }
  127.  
  128. // Attach observer to new container if needed
  129. function attachObserver() {
  130. const c = document.querySelector("main#main") || document.querySelector(".chat-container") || null;
  131. if (c !== chatContainer) {
  132. chatContainer = c;
  133. if (observer) {
  134. observer.disconnect();
  135. observer = null;
  136. }
  137. if (chatContainer) {
  138. observer = new MutationObserver(function () {
  139. debounceBuildTOC();
  140. });
  141. observer.observe(chatContainer, {childList: true, subtree: true});
  142. buildTOC();
  143. }
  144. }
  145. }
  146.  
  147. // Attempt to attach on load
  148. attachObserver();
  149. // Re-check every 2s in case container changes
  150. const reAttachInterval = setInterval(attachObserver, 2000);
  151.  
  152. // Panel toggle
  153. handle.addEventListener("click", function () {
  154. panel.classList.toggle("visible");
  155. });
  156. })();