// ==UserScript==
// @name Claude Chat TOC Navigator
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Adds a Table of Contents to navigate between responses in Claude chat
// @author You
// @match https://*.anthropic.com/*
// @match *://claude.ai/*
// @icon https://claude.ai/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Config
const config = {
updateInterval: 1000, // Check for new messages every 1 second
tocWidth: '250px',
tocMaxHeight: '80vh',
accentColor: '#6952dc',
togglerSize: '36px',
animationSpeed: '0.3s',
darkMode: {
tocBgColor: '#1e1e1e',
tocBorderColor: '#333',
textColor: '#e0e0e0',
userBgColor: '#2a2a2a',
claudeBgColor: '#2d2540',
userBorderColor: '#444',
claudeBorderColor: '#4e3d80'
},
lightMode: {
tocBgColor: '#f8f9fa',
tocBorderColor: '#ddd',
textColor: '#333',
userBgColor: '#eef0f3',
claudeBgColor: '#f4f1fe',
userBorderColor: '#d0d5dd',
claudeBorderColor: '#d1c7f9'
}
};
// Check if dark mode is enabled
function isDarkMode() {
// Check if the page has a dark background
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ||
document.body.classList.contains('dark-mode') ||
getComputedStyle(document.body).backgroundColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i)?.slice(1).map(Number).reduce((a, b) => a + b) < 382;
}
// Get theme based on current color scheme
function getTheme() {
return isDarkMode() ? config.darkMode : config.lightMode;
}
// Wait for the chat content to load
function waitForChat() {
const interval = setInterval(() => {
// Different DOM selectors for claude.ai vs anthropic.com
if (document.querySelector('.flex-1.flex.flex-col.gap-3') ||
document.querySelector('.conversation-container') ||
document.querySelector('.message-container')) {
clearInterval(interval);
initTOC();
// Listen for theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
// Additional listener for custom theme toggles
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class' ||
mutation.attributeName === 'data-theme' ||
mutation.type === 'attributes') {
updateTheme();
}
});
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'data-theme']
});
}
}, 500);
}
// Update theme for TOC elements
function updateTheme() {
const tocContainer = document.getElementById('claude-toc-container');
if (!tocContainer) return;
const theme = getTheme();
// Update container
tocContainer.style.background = theme.tocBgColor;
tocContainer.style.borderColor = theme.tocBorderColor;
tocContainer.style.color = theme.textColor;
// Update header
const header = tocContainer.querySelector('h3');
if (header) {
header.style.borderBottom = `1px solid ${theme.tocBorderColor}`;
}
// Force TOC update to refresh entry styles
updateTOC(true);
}
// Initialize the TOC
function initTOC() {
const theme = getTheme();
// Create TOC container
const tocContainer = document.createElement('div');
tocContainer.id = 'claude-toc-container';
tocContainer.style.cssText = `
position: fixed;
top: 70px;
right: -${config.tocWidth};
width: ${config.tocWidth};
max-height: ${config.tocMaxHeight};
background: ${theme.tocBgColor};
border-left: 1px solid ${theme.tocBorderColor};
border-bottom: 1px solid ${theme.tocBorderColor};
border-radius: 0 0 0 8px;
overflow-y: auto;
z-index: 1000;
transition: right ${config.animationSpeed} ease;
box-shadow: -2px 2px 10px rgba(0, 0, 0, 0.1);
padding: 10px;
color: ${theme.textColor};
`;
// Create TOC toggler with Claude logo
const tocToggler = document.createElement('div');
tocToggler.id = 'claude-toc-toggler';
tocToggler.innerHTML = `
<svg width="20" height="20" viewBox="0 0 139 34" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M18.07 30.79c-5.02 0-8.46-2.8-10.08-7.11a19.2 19.2 0 0 1-1.22-7.04C6.77 9.41 10 4.4 17.16 4.4c4.82 0 7.78 2.1 9.48 7.1h2.06l-.28-6.9c-2.88-1.86-6.48-2.81-10.87-2.81-6.16 0-11.41 2.77-14.34 7.74A16.77 16.77 0 0 0 1 18.2c0 5.53 2.6 10.42 7.5 13.15a17.51 17.51 0 0 0 8.74 2.06c4.78 0 8.57-.91 11.93-2.5l.87-7.62h-2.1c-1.26 3.48-2.76 5.57-5.25 6.68-1.22.55-2.76.83-4.62.83Z"/>
</svg>
`;
tocToggler.style.cssText = `
position: fixed;
top: 70px;
right: 0;
width: ${config.togglerSize};
height: ${config.togglerSize};
background: ${config.accentColor};
border-radius: 8px 0 0 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1001;
color: white;
box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
`;
// Create TOC header
const tocHeader = document.createElement('div');
tocHeader.innerHTML = `<h3 style="margin: 0 0 10px 0; padding-bottom: 5px; border-bottom: 1px solid ${theme.tocBorderColor};">Table of Contents</h3>`;
// Create TOC content
const tocContent = document.createElement('div');
tocContent.id = 'claude-toc-content';
tocContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
// Assemble TOC
tocContainer.appendChild(tocHeader);
tocContainer.appendChild(tocContent);
// Add to document
document.body.appendChild(tocToggler);
document.body.appendChild(tocContainer);
// Toggle TOC visibility
let tocVisible = false;
tocToggler.addEventListener('click', () => {
tocVisible = !tocVisible;
tocContainer.style.right = tocVisible ? '0' : `-${config.tocWidth}`;
// Use Claude logo consistently, just rotate it when panel is open
tocToggler.style.transform = tocVisible ? 'rotate(180deg)' : 'rotate(0deg)';
// Update theme when toggling (in case it changed)
if (tocVisible) {
const theme = getTheme();
tocContainer.style.background = theme.tocBgColor;
tocContainer.style.borderColor = theme.tocBorderColor;
tocContainer.style.color = theme.textColor;
// Also update header border color
tocContainer.querySelector('h3').style.borderBottom = `1px solid ${theme.tocBorderColor}`;
// Refresh TOC entries to apply current theme
updateTOC();
}
});
// Start monitoring for messages
updateTOC();
setInterval(updateTOC, config.updateInterval);
}
// Update the TOC content
function updateTOC(forceRefresh = false) {
const tocContent = document.getElementById('claude-toc-content');
if (!tocContent) return;
// Get all messages
let messages = [];
// User messages (questions) - support both claude.ai and anthropic.com
const userSelectors = [
'.group.relative.inline-flex.gap-2.bg-bg-300.rounded-xl', // anthropic.com
'.user-message', // alternative
'[data-message-author="user"]', // claude.ai
'.message.user' // another alternative
];
let userMessages = [];
for (const selector of userSelectors) {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
userMessages = Array.from(elements);
break;
}
}
userMessages.forEach((msg, index) => {
let textElement = msg.querySelector('[data-testid="user-message"]') ||
msg.querySelector('.message-content') ||
msg;
const textContent = textElement?.textContent?.trim();
if (textContent) {
messages.push({
type: 'user',
content: truncateText(textContent, 50),
element: msg,
index: index
});
}
});
// Claude responses - support both claude.ai and anthropic.com
const claudeSelectors = [
'.group.relative.-tracking-\\[0\\.015em\\]', // anthropic.com
'.claude-message', // alternative
'[data-message-author="assistant"]', // claude.ai
'.message.assistant' // another alternative
];
let claudeMessages = [];
for (const selector of claudeSelectors) {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
claudeMessages = Array.from(elements);
break;
}
}
claudeMessages.forEach((msg, index) => {
// Get the first paragraph of Claude's response
let textElement = msg.querySelector('.whitespace-pre-wrap.break-words') ||
msg.querySelector('.message-content') ||
msg.querySelector('p');
const textContent = textElement?.textContent?.trim();
if (textContent) {
messages.push({
type: 'claude',
content: truncateText(textContent, 50),
element: msg,
index: index
});
}
});
// Sort messages by their position in the DOM
messages.sort((a, b) => {
const posA = getElementPosition(a.element);
const posB = getElementPosition(b.element);
return posA - posB;
});
// Create TOC entries
const currentEntries = tocContent.querySelectorAll('.toc-entry');
if (currentEntries.length !== messages.length || forceRefresh) {
// Clear and rebuild TOC if message count changed
tocContent.innerHTML = '';
const theme = getTheme();
messages.forEach((msg, index) => {
const entry = document.createElement('div');
entry.className = 'toc-entry';
entry.dataset.index = index;
entry.dataset.type = msg.type;
// Styled based on message type
const bgColor = msg.type === 'user' ? theme.userBgColor : theme.claudeBgColor;
const borderColor = msg.type === 'user' ? theme.userBorderColor : theme.claudeBorderColor;
entry.style.cssText = `
padding: 8px 10px;
border-radius: 6px;
background-color: ${bgColor};
border-left: 3px solid ${borderColor};
cursor: pointer;
font-size: 13px;
font-weight: normal;
color: ${theme.textColor};
transition: all 0.2s ease;
margin-bottom: 6px;
`;
// Add sequential number and icon prefix based on message type
const msgNumber = index + 1;
const prefix = msg.type === 'user' ?
`<span style="font-weight: bold; margin-right: 5px;">${msgNumber}.</span> 👤 ` :
`<span style="font-weight: bold; margin-right: 5px;">${msgNumber}.</span> <svg width="14" height="14" viewBox="0 0 139 34" fill="currentColor" style="display: inline-block; vertical-align: middle; margin-right: 3px;"><path d="M18.07 30.79c-5.02 0-8.46-2.8-10.08-7.11a19.2 19.2 0 0 1-1.22-7.04C6.77 9.41 10 4.4 17.16 4.4c4.82 0 7.78 2.1 9.48 7.1h2.06l-.28-6.9c-2.88-1.86-6.48-2.81-10.87-2.81-6.16 0-11.41 2.77-14.34 7.74A16.77 16.77 0 0 0 1 18.2c0 5.53 2.6 10.42 7.5 13.15a17.51 17.51 0 0 0 8.74 2.06c4.78 0 8.57-.91 11.93-2.5l.87-7.62h-2.1c-1.26 3.48-2.76 5.57-5.25 6.68-1.22.55-2.76.83-4.62.83Z"/></svg> `;
entry.innerHTML = `${prefix}${msg.content}`;
// Add click event to scroll to message
entry.addEventListener('click', () => {
msg.element.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Highlight the message briefly
highlightElement(msg.element);
});
// Hover effect
entry.addEventListener('mouseenter', () => {
entry.style.transform = 'translateX(3px)';
entry.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
entry.style.fontWeight = 'bold';
});
entry.addEventListener('mouseleave', () => {
entry.style.transform = 'translateX(0)';
entry.style.boxShadow = 'none';
entry.style.fontWeight = 'normal';
});
tocContent.appendChild(entry);
});
}
}
// Helper function to get element's vertical position
function getElementPosition(element) {
return element.getBoundingClientRect().top + window.scrollY;
}
// Helper function to truncate text
function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substr(0, maxLength) + '...';
}
// Helper function to highlight an element briefly
function highlightElement(element) {
const originalBackground = element.style.background;
const originalTransition = element.style.transition;
element.style.transition = 'background-color 0.5s ease';
element.style.backgroundColor = isDarkMode()
? 'rgba(105, 82, 220, 0.3)'
: 'rgba(105, 82, 220, 0.15)';
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
setTimeout(() => {
element.style.backgroundColor = originalBackground;
setTimeout(() => {
element.style.transition = originalTransition;
}, 500);
}, 1000);
}
// Start the script
waitForChat();
})();