Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save khuongsatou/9e9f5a0464113332061bdc9f3d412b3c to your computer and use it in GitHub Desktop.
Save khuongsatou/9e9f5a0464113332061bdc9f3d412b3c 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");
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment