您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a floating Table of Contents with code block detection for navigating chat messages in Google AI Studio
当前为
// ==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); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址