// ==UserScript==
// @name Enhanced Google AI Studio Chat Navigator
// @namespace http://tampermonkey.net/
// @version 2.0
// @description Adds a floating Table of Contents with code block detection for navigating chat messages in Google AI Studio
// @author Claude
// @match https://aistudio.google.com/prompts/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
/*
* This script adds a floating navigation button to Google AI Studio that displays
* a table of contents for all chat messages in the conversation.
*
* Features:
* - Shows both user messages and AI responses in chronological order
* - Each item is numbered sequentially from top to bottom
* - Displays file attachments along with regular chat messages
* - Detects and lists code blocks as sub-items under their parent messages
* - Clicking an item scrolls to that message and highlights it
* - Supports both light and dark themes
*/
(function() {
'use strict';
// Configuration
const config = {
buttonPosition: { bottom: '90px', right: '18px' },
buttonStyle: {
width: '28px',
height: '28px',
borderRadius: '50%',
background: '#4285F4',
color: 'white',
border: 'none',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
cursor: 'pointer',
zIndex: '9999',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px'
},
tocPanelStyle: {
position: 'fixed',
top: '60px',
right: '16px',
width: '280px',
maxHeight: 'calc(100vh - 120px)',
background: '#1e1e1e',
color: '#e0e0e0',
borderRadius: '6px',
boxShadow: '0 2px 10px rgba(0,0,0,0.3)',
zIndex: '9998',
overflowY: 'auto',
display: 'none',
padding: '10px 8px',
fontFamily: 'Google Sans, Roboto, sans-serif'
},
darkModeClass: 'dark-theme'
};
// Create and append styles
function addStyles() {
try {
const style = document.createElement('style');
style.textContent = `
.chat-navigator-toc {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: translateY(10px);
opacity: 0;
scrollbar-width: thin;
}
.chat-navigator-toc::-webkit-scrollbar {
width: 6px;
}
.chat-navigator-toc::-webkit-scrollbar-track {
background: #2d2d2d;
}
.chat-navigator-toc::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
.chat-navigator-toc.visible {
transform: translateY(0);
opacity: 1;
}
.chat-navigator-item {
padding: 4px 6px;
margin: 2px 0;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
border-left-width: 2px;
font-size: 10px;
}
.chat-navigator-item:hover {
background-color: #333;
}
.chat-navigator-item.user {
border-left: 4px solid #4285F4;
}
.chat-navigator-item.agent {
border-left: 4px solid #34A853;
}
.chat-navigator-item-icon {
margin-right: 4px;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.chat-navigator-item-icon .material-symbols-outlined {
font-size: 12px;
font-weight: normal;
}
.chat-navigator-item-text {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #e0e0e0;
line-height: 1.2;
}
.chat-navigator-toc-header {
font-size: 11px;
font-weight: 500;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #3d3d3d;
display: flex;
justify-content: space-between;
align-items: center;
color: #e0e0e0;
}
.chat-navigator-close {
cursor: pointer;
padding: 2px;
border-radius: 50%;
font-size: 14px;
}
.chat-navigator-close:hover {
background-color: #3d3d3d;
}
@keyframes highlightFade {
0% { opacity: 1; }
100% { opacity: 0; }
}
.chat-navigator-user-icon, .chat-navigator-ai-icon {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 4px;
flex-shrink: 0;
}
.chat-navigator-user-icon {
background-color: #bbb;
color: #222;
font-size: 8px;
}
.chat-navigator-ai-icon {
background-color: #4285F4;
color: white;
font-size: 8px;
}
/* Code block items styling */
.chat-navigator-code-item {
padding: 3px 6px 3px 24px;
margin: 1px 0;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
border-left: 3px solid #F9AB00;
font-size: 9px;
}
.chat-navigator-code-item:hover {
background-color: #333;
}
.chat-navigator-code-icon {
margin-right: 4px;
width: 11px;
height: 11px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #F9AB00;
font-size: 7px;
background-color: rgba(249, 171, 0, 0.2);
border-radius: 3px;
}
/* Dark mode support */
.${config.darkModeClass} .chat-navigator-toc {
background-color: #1e1e1e;
color: #e0e0e0;
border: 1px solid #3d3d3d;
}
.${config.darkModeClass} .chat-navigator-item:hover,
.${config.darkModeClass} .chat-navigator-code-item:hover {
background-color: #333;
}
.${config.darkModeClass} .chat-navigator-toc-header {
border-bottom-color: #3d3d3d;
color: #e0e0e0;
}
.${config.darkModeClass} .chat-navigator-close:hover {
background-color: #3d3d3d;
}
.${config.darkModeClass} .chat-navigator-item-text {
color: #e0e0e0;
}
/* Light mode support */
.chat-navigator-toc:not(.${config.darkModeClass}) {
background-color: white;
color: #333;
border: 1px solid #e0e0e0;
}
.chat-navigator-toc:not(.${config.darkModeClass}) .chat-navigator-item-text {
color: #333;
}
.chat-navigator-toc:not(.${config.darkModeClass}) .chat-navigator-item:hover,
.chat-navigator-toc:not(.${config.darkModeClass}) .chat-navigator-code-item:hover {
background-color: #f1f3f4;
}
.chat-navigator-toc:not(.${config.darkModeClass}) .chat-navigator-toc-header {
border-bottom-color: #e0e0e0;
color: #333;
}
.${config.darkModeClass} .chat-navigator-item-icon .material-symbols-outlined {
color: #e0e0e0;
}
/* Collapsible section controls */
.chat-navigator-toggle {
cursor: pointer;
font-size: 10px;
margin-left: auto;
padding: 0 3px;
border-radius: 3px;
color: #999;
}
.chat-navigator-toggle:hover {
color: #ccc;
background-color: rgba(255, 255, 255, 0.1);
}
.chat-navigator-collapsed .chat-navigator-code-items-container {
display: none;
}
.chat-navigator-copy-btn {
margin-left: 4px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
opacity: 0.7;
}
.chat-navigator-copy-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
opacity: 1;
}
`;
document.head.appendChild(style);
} catch (err) {
console.error('Error adding styles:', err);
}
}
// Create the floating TOC button
function createTocButton() {
try {
const button = document.createElement('button');
button.id = 'chat-navigator-button';
button.title = 'Chat Navigator';
button.setAttribute('aria-label', 'Open Chat Navigator');
// Apply button styles
Object.assign(button.style, config.buttonStyle, {
position: 'fixed',
...config.buttonPosition
});
// Add icon safely with DOM methods
const iconSpan = document.createElement('span');
iconSpan.className = 'material-symbols-outlined notranslate';
iconSpan.textContent = 'menu'; // Changed from 'list' to 'menu' for better appearance
iconSpan.style.fontSize = '14px'; // Smaller icon size
iconSpan.style.fontWeight = 'normal'; // Normal weight for icon
button.appendChild(iconSpan);
// Add click event
button.addEventListener('click', toggleTocPanel);
document.body.appendChild(button);
return button;
} catch (err) {
console.error('Error creating TOC button:', err);
return null;
}
}
// Create the TOC panel
function createTocPanel() {
try {
const panel = document.createElement('div');
panel.id = 'chat-navigator-toc';
panel.className = 'chat-navigator-toc';
// Apply panel styles
Object.assign(panel.style, config.tocPanelStyle);
// Create header using safe DOM methods
const header = document.createElement('div');
header.className = 'chat-navigator-toc-header';
// Add title text
const titleSpan = document.createElement('span');
titleSpan.textContent = 'Chat Navigator';
header.appendChild(titleSpan);
// Add close button
const closeSpan = document.createElement('span');
closeSpan.className = 'chat-navigator-close material-symbols-outlined notranslate';
closeSpan.textContent = 'close';
closeSpan.style.fontSize = '14px';
closeSpan.style.fontWeight = 'normal';
header.appendChild(closeSpan);
// Create content container
const content = document.createElement('div');
content.className = 'chat-navigator-toc-content';
panel.appendChild(header);
panel.appendChild(content);
// Add close button event
closeSpan.addEventListener('click', toggleTocPanel);
document.body.appendChild(panel);
return panel;
} catch (err) {
console.error('Error creating TOC panel:', err);
return null;
}
}
// Toggle TOC panel visibility
function toggleTocPanel() {
try {
const panel = document.getElementById('chat-navigator-toc');
if (!panel) {
console.warn('TOC panel not found');
return;
}
const isVisible = panel.style.display === 'block';
if (isVisible) {
panel.style.display = 'none';
panel.classList.remove('visible');
} else {
// Update TOC content before showing
updateTocContent();
panel.style.display = 'block';
setTimeout(() => {
panel.classList.add('visible');
}, 10);
}
} catch (err) {
console.error('Error toggling TOC panel:', err);
}
}
// Toggle code blocks visibility
function toggleCodeBlocks(event, itemId) {
try {
const item = document.getElementById(itemId);
if (item) {
item.classList.toggle('chat-navigator-collapsed');
// Update toggle icon
const toggle = event.currentTarget;
toggle.textContent = item.classList.contains('chat-navigator-collapsed') ? 'expand_more' : 'expand_less';
}
// Prevent the click from propagating to the parent item
event.stopPropagation();
} catch (err) {
console.error('Error toggling code blocks:', err);
}
}
// Update TOC content based on current chat messages
function updateTocContent() {
try {
const panel = document.getElementById('chat-navigator-toc');
if (!panel) {
console.warn('TOC panel not found for updating content');
return;
}
const content = panel.querySelector('.chat-navigator-toc-content');
if (!content) {
console.warn('TOC content container not found');
return;
}
// Clear previous content
while (content.firstChild) {
content.removeChild(content.firstChild);
}
// Try multiple selectors to find chat turns
const selectors = [
'ms-chat-turn',
'.chat-turn-container',
'.turn-content',
'.user-prompt-container, .model-prompt-container',
'.chat-message'
];
let chatTurns = [];
for (const selector of selectors) {
chatTurns = document.querySelectorAll(selector);
if (chatTurns.length > 0) {
console.log(`Found ${chatTurns.length} chat turns using selector: ${selector}`);
break;
}
}
if (chatTurns.length === 0) {
const noMessagesDiv = document.createElement('div');
noMessagesDiv.style.padding = '8px';
noMessagesDiv.textContent = 'No chat messages found';
content.appendChild(noMessagesDiv);
return;
}
// Process each chat turn
chatTurns.forEach((turn, index) => {
try {
// Determine if user or AI message
// Try multiple ways to detect the role
let isUser = false;
if (turn.querySelector('.user-prompt-container')) {
isUser = true;
} else if (turn.classList.contains('user')) {
isUser = true;
} else if (turn.getAttribute('data-turn-role') === 'User') {
isUser = true;
} else if (turn.closest('.user-message')) {
isUser = true;
}
const role = isUser ? 'user' : 'agent';
// Create unique ID for this chat item
const chatItemId = `chat-item-${index}`;
// Extract content snippet for the TOC entry
let snippet = getContentSnippet(turn, role);
// Create TOC item
const item = document.createElement('div');
item.className = `chat-navigator-item ${role}`;
item.id = chatItemId;
item.dataset.index = index;
item.dataset.serialNumber = index + 1; // Store the serial number for reference
// Create elements using safe DOM methods
const iconDiv = document.createElement('div');
iconDiv.className = 'chat-navigator-item-icon';
// Use custom user/AI icons instead of material icons
if (isUser) {
const userIcon = document.createElement('div');
userIcon.className = 'chat-navigator-user-icon';
userIcon.textContent = 'U';
iconDiv.appendChild(userIcon);
} else {
const aiIcon = document.createElement('div');
aiIcon.className = 'chat-navigator-ai-icon';
aiIcon.textContent = 'AI';
iconDiv.appendChild(aiIcon);
}
const textDiv = document.createElement('div');
textDiv.className = 'chat-navigator-item-text';
// Add serial number prefix to each item
const serialNum = (index + 1).toString().padStart(2, '0');
// Extract the first few meaningful words from the message
if (snippet.length > 5 && !snippet.startsWith('[')) {
// Clean up common prefixes
snippet = snippet.replace(/^(User message|AI response|User input):\s*/i, '');
}
textDiv.textContent = `${serialNum}. ${snippet}`;
// Create a container for the main item elements
const itemContent = document.createElement('div');
itemContent.style.display = 'flex';
itemContent.style.flexGrow = '1';
itemContent.style.alignItems = 'center';
itemContent.appendChild(iconDiv);
itemContent.appendChild(textDiv);
item.appendChild(itemContent);
// Find code blocks in the message
const codeBlocks = findCodeBlocks(turn);
// If there are code blocks, add a toggle control
if (codeBlocks.length > 0) {
const toggleDiv = document.createElement('div');
toggleDiv.className = 'chat-navigator-toggle material-symbols-outlined notranslate';
toggleDiv.textContent = 'expand_less'; // Default to expanded
toggleDiv.addEventListener('click', (e) => toggleCodeBlocks(e, chatItemId));
item.appendChild(toggleDiv);
}
// Add click event to scroll to message
item.addEventListener('click', () => {
scrollToMessage(turn);
// Don't close the panel when clicking on a parent item
});
content.appendChild(item);
// Add code blocks as sub-items if any were found
if (codeBlocks.length > 0) {
const codeItemsContainer = document.createElement('div');
codeItemsContainer.className = 'chat-navigator-code-items-container';
codeBlocks.forEach((codeData, codeIndex) => {
try {
const codeItem = document.createElement('div');
codeItem.className = 'chat-navigator-code-item';
const codeIconDiv = document.createElement('div');
codeIconDiv.className = 'chat-navigator-code-icon';
codeIconDiv.textContent = '</>';
const codeTextDiv = document.createElement('div');
codeTextDiv.className = 'chat-navigator-item-text';
// Add a prefix based on the language if available
let language = codeData.language || 'Code';
if (language.toLowerCase() === 'code') {
language = detectLanguage(codeData.text);
}
// Add serial number to code blocks
const serialNum = ((index + 1) + "." + (codeIndex + 1)).toString().padStart(4, '0');
let codeSnippet = codeData.text.trim().split('\n')[0].substring(0, 25) || 'Code block';
// Check if this looks like Java code by searching for package or import statements
// or class declarations at the beginning of lines
const codeText = codeData.text.trim();
let packageName = null;
let fullClassName = null;
const looksLikeJava = /^package\s+[\w.]+;|^import\s+[\w.*]+;|^(public\s+|private\s+|protected\s+)?(abstract\s+|final\s+)?\s*class\s+\w+/m.test(codeText);
if (looksLikeJava) {
// Extract package name if present
const packageMatch = codeText.match(/package\s+([\w.]+);/);
if (packageMatch && packageMatch[1]) {
packageName = packageMatch[1];
}
// Look for class declaration pattern
const classMatch = codeText.match(/class\s+(\w+)[\s{]/);
if (classMatch && classMatch[1]) {
// Use the class name if found
codeSnippet = `${classMatch[1]}`;
language = 'Java'; // Override language detection
// Create fully qualified class name if package exists
if (packageName) {
fullClassName = `${packageName}.${classMatch[1]}`;
} else {
fullClassName = classMatch[1];
}
}
}
if (language && language.toLowerCase() === 'java') {
codeTextDiv.textContent = `${serialNum}. ${codeSnippet}`;
} else {
codeTextDiv.textContent = `${serialNum}. ${language}: ${codeSnippet}...`;
}
codeItem.appendChild(codeIconDiv);
codeItem.appendChild(codeTextDiv);
// Create copy button for code
const copyBtn = document.createElement('div');
copyBtn.className = 'chat-navigator-copy-btn';
copyBtn.textContent = '📋'; // Unicode clipboard symbol
copyBtn.title = 'Copy code';
// Add click event for copying code
copyBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent navigation to the code block
navigator.clipboard.writeText(codeData.text)
.then(() => {
// Show temporary feedback
const originalText = copyBtn.textContent;
copyBtn.textContent = '✓';
setTimeout(() => {
copyBtn.textContent = originalText;
}, 1500);
})
.catch(err => {
console.error('Failed to copy code:', err);
});
});
codeItem.appendChild(copyBtn);
// Add full class name copy button for Java classes
if (fullClassName) {
const copyClassNameBtn = document.createElement('div');
copyClassNameBtn.className = 'chat-navigator-copy-btn';
copyClassNameBtn.textContent = '⊕'; // Different symbol for class name copy
copyClassNameBtn.title = 'Copy fully qualified class name';
copyClassNameBtn.style.marginLeft = '2px';
// Add click event for copying class name
copyClassNameBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent navigation to the code block
navigator.clipboard.writeText(fullClassName)
.then(() => {
// Show temporary feedback
const originalText = copyClassNameBtn.textContent;
copyClassNameBtn.textContent = '✓';
setTimeout(() => {
copyClassNameBtn.textContent = originalText;
}, 1500);
})
.catch(err => {
console.error('Failed to copy class name:', err);
});
});
codeItem.appendChild(copyClassNameBtn);
}
/*codeItem.appendChild(codeIconDiv);
codeItem.appendChild(copyBtn);
codeItem.appendChild(codeTextDiv); */
// Add click event to scroll to the specific code block
codeItem.addEventListener('click', () => {
scrollToElement(codeData.element);
// Don't close TOC when navigating to keep context
});
codeItemsContainer.appendChild(codeItem);
} catch (err) {
console.warn('Error creating code item:', err);
}
});
// Add the container after the main chat item
content.appendChild(codeItemsContainer);
}
} catch (err) {
console.warn(`Error processing chat turn ${index}:`, err);
}
});
} catch (err) {
console.error('Error updating TOC content:', err);
}
}
// Extract content snippet from chat turn
function getContentSnippet(turn, role) {
try {
let text = '';
if (role === 'user') {
// Try to extract user text
const textElem = turn.querySelector('.turn-content, .message-content');
if (textElem) {
// Check for text content
text = textElem.textContent.trim();
// Check for file attachments
const fileChunk = turn.querySelector('ms-file-chunk, .file-attachment');
if (fileChunk) {
const fileName = fileChunk.querySelector('.name, .filename')?.textContent || 'File';
text = `[${fileName}]`;
}
// If still empty, check for other content types
if (!text) {
const hasContent = textElem.querySelector('*');
text = hasContent ? 'User input' : 'Empty message';
}
}
} else {
// AI message
const contentElem = turn.querySelector('.render.agent .turn-content, .model-response, .message-content');
if (contentElem) {
text = contentElem.textContent.trim();
// Handle code blocks or other special content
if (!text) {
if (contentElem.querySelector('pre') || contentElem.querySelector('code')) {
text = 'Code block';
} else if (contentElem.querySelector('img')) {
text = 'Image';
} else if (contentElem.querySelector('table')) {
text = 'Table';
} else {
text = 'AI response';
}
}
}
}
// Extract first few meaningful words for better identification
if (text.length > 5) {
// Remove common prefixes that don't add value
text = text.replace(/^(User message|AI response|User input):\s*/i, '');
// Split into words and take first few
const words = text.split(/\s+/);
if (words.length > 3) {
// Take up to 4-5 meaningful words
text = words.slice(0, 4).join(' ');
if (text.length < 20 && words.length > 4) {
text += ' ' + words[4];
}
text += '...';
}
}
// Limit text length
return text.length > 30 ? text.substring(0, 30) + '...' : text || (role === 'user' ? 'User input' : 'AI response');
} catch (err) {
console.warn('Error getting content snippet:', err);
return role === 'user' ? 'User input' : 'AI response';
}
}
// Find code blocks in a message
function findCodeBlocks(turn) {
const codeBlocks = [];
try {
// Check for pre elements (code blocks)
const preElements = turn.querySelectorAll('pre');
preElements.forEach(pre => {
try {
// Check if there's a code element inside the pre
const codeElement = pre.querySelector('code');
if (codeElement) {
// Try to detect language from class name (common pattern: language-xyz)
let language = 'Code';
const classes = codeElement.className.split(' ');
for (const cls of classes) {
if (cls.startsWith('language-')) {
language = cls.replace('language-', '');
// Capitalize first letter
language = language.charAt(0).toUpperCase() + language.slice(1);
break;
}
}
codeBlocks.push({
element: pre,
text: codeElement.textContent || pre.textContent,
language: language
});
} else {
// If no code element, use the pre element itself
codeBlocks.push({
element: pre,
text: pre.textContent,
language: 'Code'
});
}
} catch (err) {
console.warn('Error processing pre element:', err);
}
});
// Check for Google AI Studio specific code elements
// These are common patterns in Google AI Studio's DOM structure
const studioCodeBlocks = turn.querySelectorAll('.code-block-wrapper, .code-wrapper, .render pre, [data-code=true]');
studioCodeBlocks.forEach(block => {
try {
if (!codeBlocks.some(existing => existing.element === block)) { // Avoid duplicates
codeBlocks.push({
element: block,
text: block.textContent,
language: 'Code'
});
}
} catch (err) {
console.warn('Error processing studio code block:', err);
}
});
// Also check for inline code elements that are not inside pre blocks
const inlineCodeElements = turn.querySelectorAll('code:not(pre code)');
inlineCodeElements.forEach(code => {
try {
if (code.textContent.trim().length > 0) {
codeBlocks.push({
element: code,
text: code.textContent,
language: 'Inline'
});
}
} catch (err) {
console.warn('Error processing inline code element:', err);
}
});
// Check for special code renderers (Gemini sometimes uses these)
const customCodeBlocks = turn.querySelectorAll('.code-block, .language-*, [data-code-block]');
customCodeBlocks.forEach(block => {
try {
if (!block.closest('pre') && !codeBlocks.some(existing => existing.element === block)) { // Avoid duplicates
// Look for language indicator
let language = 'Code';
// Check if the block has a language indicator
const langIndicator = block.querySelector('.language-indicator, [data-language]');
if (langIndicator) {
language = langIndicator.textContent || langIndicator.getAttribute('data-language') || 'Code';
}
codeBlocks.push({
element: block,
text: block.textContent,
language: language
});
}
} catch (err) {
console.warn('Error processing custom code block:', err);
}
});
} catch (err) {
console.error('Error finding code blocks:', err);
}
return codeBlocks;
}
// Try to detect language from code content
function detectLanguage(code) {
try {
const firstLine = code.trim().split('\n')[0].toLowerCase();
// Simple language detection based on first line
if (firstLine.includes('python') || firstLine.startsWith('import ') || firstLine.startsWith('from ') || firstLine.includes('def ')) {
return 'Python';
} else if (firstLine.includes('javascript') || firstLine.includes('const ') || firstLine.includes('let ') || firstLine.includes('function ')) {
return 'JavaScript';
} else if (firstLine.includes('html') || firstLine.includes('<!doctype') || firstLine.includes('<html')) {
return 'HTML';
} else if (firstLine.includes('css') || firstLine.includes('{') && firstLine.includes(':')) {
return 'CSS';
} else if (firstLine.includes('java') || firstLine.includes('public class')) {
return 'Java';
} else if (firstLine.includes('sql') || firstLine.includes('select ') || firstLine.includes('create table')) {
return 'SQL';
} else if (firstLine.includes('bash') || firstLine.startsWith('#!') || firstLine.startsWith('#!/')) {
return 'Bash';
}
return 'Code';
} catch (err) {
console.warn('Error detecting language:', err);
return 'Code';
}
}
// Scroll to specific message
function scrollToMessage(element) {
try {
if (!element) {
console.warn('No element provided to scroll to');
return;
}
scrollToElement(element);
} catch (err) {
console.error('Error scrolling to message:', err);
}
}
// Scroll to a specific element with highlight effect
// Scroll to a specific element with highlight effect
function scrollToElement(element) {
try {
if (!element) {
console.warn('No element provided to scroll to');
return;
}
// Scroll element into view with smooth animation
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Add highlight effect
const highlight = document.createElement('div');
Object.assign(highlight.style, {
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
backgroundColor: 'rgba(66, 133, 244, 0.15)', // Lighter blue highlight for better visibility
borderRadius: '4px',
pointerEvents: 'none',
zIndex: '1',
animation: 'highlightFade 1.5s ease-out forwards'
});
// Position the element relatively if needed
const currentPosition = window.getComputedStyle(element).position;
if (currentPosition === 'static') {
element.style.position = 'relative';
}
element.appendChild(highlight);
setTimeout(() => {
if (element.contains(highlight)) {
element.removeChild(highlight);
}
if (currentPosition === 'static') {
element.style.position = '';
}
}, 1500);
} catch (err) {
console.error('Error in scrollToElement:', err);
}
}
// Check if dark mode is active
function isDarkMode() {
try {
return document.body.classList.contains('dark-theme') ||
document.documentElement.classList.contains('dark-theme') ||
document.body.classList.contains('dark') ||
document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch (err) {
console.warn('Error checking dark mode:', err);
return true; // Default to dark mode for safety
}
}
// Update dark mode status
function updateDarkMode() {
try {
const panel = document.getElementById('chat-navigator-toc');
if (!panel) {
console.warn('TOC panel not found for dark mode update');
return;
}
if (isDarkMode()) {
panel.classList.add(config.darkModeClass);
} else {
panel.classList.remove(config.darkModeClass);
}
} catch (err) {
console.error('Error updating dark mode:', err);
}
}
// Initialize the script
function init() {
try {
console.log('Initializing Enhanced Google AI Studio Chat Navigator...');
// Add CSS styles
addStyles();
// Create UI components
createTocButton();
createTocPanel();
// Force dark mode as we're using dark theme for Google AI Studio
const tocPanel = document.getElementById('chat-navigator-toc');
if (tocPanel) {
tocPanel.classList.add(config.darkModeClass);
} else {
console.warn('TOC panel not found after creation');
}
// Set up dark mode detection
updateDarkMode();
// Watch for theme changes
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'class') {
updateDarkMode();
}
}
});
observer.observe(document.body, { attributes: true });
observer.observe(document.documentElement, { attributes: true });
// Set up mutation observer to detect new chat messages
const chatObserver = new MutationObserver(() => {
const panel = document.getElementById('chat-navigator-toc');
if (panel && panel.style.display === 'block') {
updateTocContent();
}
});
// Start observing when chat container becomes available
function observeChatContainer() {
// Try multiple possible selectors to find the chat container
const selectors = [
'ms-chat-session',
'.chat-view-container',
'.chat-container',
'.turn-content',
'[role="main"]'
];
let chatContainer = null;
for (const selector of selectors) {
chatContainer = document.querySelector(selector);
if (chatContainer) break;
}
if (chatContainer) {
chatObserver.observe(chatContainer, {
childList: true,
subtree: true
});
console.log('Chat container observed for changes using selector: ' +
(chatContainer.tagName || 'unknown'));
// Also observe the body for larger structural changes
chatObserver.observe(document.body, {
childList: true,
subtree: false
});
} else {
console.log('Chat container not found, retrying in 1 second...');
setTimeout(observeChatContainer, 1000);
}
}
observeChatContainer();
console.log('Enhanced Google AI Studio Chat Navigator initialized');
} catch (err) {
console.error('Error in initialization:', err);
}
}
// Run initialization when document is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// Give a moment for the AI Studio UI to fully initialize
setTimeout(init, 1000);
}
})();