Skip to content

Instantly share code, notes, and snippets.

@Brostoffed
Last active April 21, 2025 02:26
Show Gist options
  • Save Brostoffed/cbe33856cb1beb5f1c3852b9b5625204 to your computer and use it in GitHub Desktop.
Save Brostoffed/cbe33856cb1beb5f1c3852b9b5625204 to your computer and use it in GitHub Desktop.
Add table of contents to ChatGPT
javascript:(function(){"use strict";if(document.getElementById("toc-panel")||document.getElementById("toc-handle"))return;var e=document.createElement("style");e.textContent="#toc-panel{position:fixed;top:0;right:0;width:280px;height:100%;background:#fafafa;box-shadow:-4px 0 8px rgba(0,0,0,.1);font-family:sans-serif;font-size:.8rem;border-left:1px solid #ddd;display:flex;flex-direction:column;z-index:9998;transform:translateX(0);transition:transform .3s ease}#toc-panel.collapsed{transform:translateX(280px)}#toc-header{padding:6px 10px;background:#ddd;border-bottom:1px solid #ccc;font-weight:bold;flex-shrink:0}#toc-list{list-style:none;flex:1;overflow-y:auto;margin:0;padding:6px}#toc-list li{padding:4px;cursor:pointer;border-radius:3px;transition:background-color .2s}#toc-list li:hover{background:#f0f0f0}#toc-list ul{margin-left:16px;padding:0}#toc-list ul li::before{content:\"\"}#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 .2s}#toc-handle:hover{background:#bbb}@keyframes highlightFade{0%{background-color:#fffa99}100%{background-color:transparent}}.toc-highlight{animation:highlightFade 1.5s forwards}@media (prefers-color-scheme:dark){#toc-panel{background:#333;border-left:1px solid #555;box-shadow:-4px 0 8px rgba(0,0,0,.7)}#toc-header{background:#555;border-bottom:1px solid #666;color:#eee}#toc-list li:hover{background:#444}#toc-list{color:#eee}#toc-handle{background:#555;color:#ddd}#toc-handle:hover{background:#666}}",document.head.appendChild(e);var t=document.createElement("div");t.id="toc-panel",t.innerHTML='<div id="toc-header">Conversation TOC</div><ul id="toc-list"></ul>',document.body.appendChild(t);var n=document.createElement("div");n.id="toc-handle",n.textContent="TOC",document.body.appendChild(n);var r=null,o=null,i=false,a=null;function c(){if(i)return;i=true;a=setTimeout(function(){l(),i=false},300)}function l(){var s=document.getElementById("toc-list");if(!s)return;s.innerHTML="";var d=(r||document).querySelectorAll("article[data-testid^='conversation-turn-']");if(!d||d.length===0){s.innerHTML='<li style="opacity:0.7;font-style:italic;">Empty chat</li>';return}for(var u=0;u<d.length;u++){var g=d[u],m=document.createElement("li"),h=g.querySelector("h6.sr-only"),f=false;if(h&&h.textContent.indexOf("ChatGPT said:")>=0){f=true,m.textContent="Turn "+(u+1)+" (AI)"}else{m.textContent="Turn "+(u+1)+" (You)"}(function(E){m.addEventListener("click",function(){E.scrollIntoView({behavior:"smooth",block:"start"})})})(g);if(f){var v=document.createElement("ul"),p=g.querySelectorAll("h3:not(.sr-only)");for(var L=0;L<p.length;L++){var x=p[L],y=false,w=x;while(w){if(w.tagName==="PRE"||w.tagName==="CODE"){y=true;break}w=w.parentElement}if(y)continue;var b=document.createElement("li"),H=(x.textContent||"").trim()||"Section "+(L+1);b.textContent=H;(function(M){b.addEventListener("click",function(A){A.stopPropagation(),M.classList.remove("toc-highlight"),M.offsetWidth,M.classList.add("toc-highlight"),M.scrollIntoView({behavior:"smooth",block:"start"})})})(x),v.appendChild(b)}v.children.length>0&&m.appendChild(v)}s.appendChild(m)}}function y(){var e=document.querySelector("main#main")||document.querySelector(".chat-container")||null;if(e!==r){r=e,o&&(o.disconnect(),o=null),r&&(o=new MutationObserver(function(){c()}),o.observe(r,{childList:true,subtree:true}),l())}}y();var I=setInterval(y,2e3);n.addEventListener("click",function(){t.classList.toggle("collapsed")});})();
javascript:(function () {
"use strict";
// If panel already exists, do nothing
if (document.getElementById("toc-panel") || document.getElementById("toc-handle")) {
return;
}
// --- Insert CSS with dark mode support ---
const css = document.createElement("style");
css.textContent = `
/* Panel */
#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;
transform: translateX(0);
transition: transform 0.3s ease;
}
#toc-panel.collapsed {
transform: translateX(280px);
}
/* Panel Header */
#toc-header {
padding: 6px 10px;
background: #ddd;
border-bottom: 1px solid #ccc;
font-weight: bold;
flex-shrink: 0;
}
/* TOC Items */
#toc-list {
list-style: none;
flex: 1;
overflow-y: auto;
margin: 0;
padding: 6px;
}
#toc-list li {
padding: 4px;
cursor: pointer;
border-radius: 3px;
transition: background-color 0.2s;
}
#toc-list li:hover {
background: #f0f0f0;
}
#toc-list ul {
margin-left: 16px;
padding: 0;
}
#toc-list ul li::before {
content: "";
}
/* Always-visible handle */
#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;
}
#toc-handle:hover {
background: #bbb;
}
/* Highlighting headings in the chat */
@keyframes highlightFade {
0% { background-color: #fffa99; }
100% { background-color: transparent; }
}
.toc-highlight {
animation: highlightFade 1.5s forwards;
}
/* ------ Dark Mode Support ------ */
@media (prefers-color-scheme: dark) {
#toc-panel {
background: #333;
border-left: 1px solid #555;
box-shadow: -4px 0 8px rgba(0,0,0,0.7);
}
#toc-header {
background: #555;
border-bottom: 1px solid #666;
color: #eee;
}
#toc-list li:hover {
background: #444;
}
#toc-list {
color: #eee;
}
#toc-handle {
background: #555;
color: #ddd;
}
#toc-handle:hover {
background: #666;
}
}
`;
document.head.appendChild(css);
// --- Create panel & handle ---
const panel = document.createElement("div");
panel.id = "toc-panel";
panel.innerHTML = `
<div id="toc-header">Conversation TOC</div>
<ul id="toc-list"></ul>
`;
document.body.appendChild(panel);
const handle = document.createElement("div");
handle.id = "toc-handle";
handle.textContent = "TOC";
document.body.appendChild(handle);
// Observed container, observer, etc.
let chatContainer = null;
let observer = null;
let isScheduled = false;
let timerId = null;
// Debounce the TOC build to avoid high CPU usage on rapid changes
function debounceBuildTOC() {
if (isScheduled) return;
isScheduled = true;
timerId = setTimeout(function () {
buildTOC();
isScheduled = false;
}, 300);
}
// Build/refresh the TOC
function buildTOC() {
const list = document.getElementById("toc-list");
if (!list) return;
list.innerHTML = "";
// Find conversation turns
const articles = (chatContainer || document).querySelectorAll("article[data-testid^='conversation-turn-']");
if (!articles || articles.length === 0) {
list.innerHTML = '<li style="opacity:0.7;font-style:italic;">Empty chat</li>';
return;
}
// Loop over turns
for (let i = 0; i < articles.length; i++) {
const art = articles[i];
const li = document.createElement("li");
// Check if AI
const sr = art.querySelector("h6.sr-only");
let isAI = false;
if (sr && sr.textContent.indexOf("ChatGPT said:") >= 0) {
isAI = true;
li.textContent = "Turn " + (i + 1) + " (AI)";
} else {
li.textContent = "Turn " + (i + 1) + " (You)";
}
// On click: scroll to turn
(function (turnElem) {
li.addEventListener("click", function () {
turnElem.scrollIntoView({behavior: "smooth", block: "start"});
});
})(art);
// AI subheadings
if (isAI) {
const subUl = document.createElement("ul");
const heads = art.querySelectorAll("h3:not(.sr-only)");
for (let h = 0; h < heads.length; h++) {
const hd = heads[h];
// Skip headings inside <pre> or <code>
let skip = false;
let p = hd;
while (p) {
if (p.tagName === "PRE" || p.tagName === "CODE") {
skip = true;
break;
}
p = p.parentElement;
}
if (skip) continue;
// Sub-item
const subLi = document.createElement("li");
const txt = (hd.textContent || "").trim() || ("Section " + (h + 1));
subLi.textContent = txt;
// Scroll + highlight
(function (hdElem) {
subLi.addEventListener("click", function (ev) {
ev.stopPropagation();
hdElem.classList.remove("toc-highlight");
// Force reflow to restart animation
hdElem.offsetWidth;
hdElem.classList.add("toc-highlight");
hdElem.scrollIntoView({behavior: "smooth", block: "start"});
});
})(hd);
subUl.appendChild(subLi);
}
if (subUl.children.length > 0) {
li.appendChild(subUl);
}
}
list.appendChild(li);
}
}
// Attach observer to new container if needed
function attachObserver() {
// Attempt to locate the main chat container
const c = document.querySelector("main#main") || document.querySelector(".chat-container") || null;
if (c !== chatContainer) {
chatContainer = c;
// Disconnect old observer if any
if (observer) {
observer.disconnect();
observer = null;
}
// Attach new observer if container found
if (chatContainer) {
observer = new MutationObserver(function () {
debounceBuildTOC();
});
observer.observe(chatContainer, {childList: true, subtree: true});
buildTOC();
}
}
}
// Attempt to attach on load
attachObserver();
// Re-check every 2s in case container changes
const reAttachInterval = setInterval(attachObserver, 2000);
// Panel toggle
handle.addEventListener("click", function () {
panel.classList.toggle("collapsed");
});
})();
@Brostoffed
Copy link
Author

Brostoffed commented Apr 2, 2025

Instructions

  • Create a new bookmark in your browser
  • Name the new bookmark what you want
  • Add the contents of [Minified] - ChatGPT-TOC.js to the Address input
  • When using ChatGPT.com, click the bookmarklet. This will make it load the TOC

Notes

  • This does not currently display or navigate edits
  • Works with Darkmode now

Visual Example

image

@minhth1529
Copy link

Hi @Brostoffed this util very help full, thank for your shared, but now i am using Arc browser, can you please create one for it?

@Brostoffed
Copy link
Author

Hi @Brostoffed this util very help full, thank for your shared, but now i am using Arc browser, can you please create one for it?

Can you please describe the problem that you're having.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment