Advertisement
Guest User

AI Studio Add Code Viewer/Comment Navigation

a guest
May 19th, 2025
28
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 27.64 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         AI Studio CodeViewer/CommentNav
  3. // @namespace    http://tampermonkey.net/
  4. // @version      0.9.21
  5. // @description  Adds code viewer and comment navigation buttons
  6. // @author       nooneisreal
  7. // @match        *://aistudio.google.com/*
  8. // @icon         https://www.gstatic.com/aistudio/branding/logo_light.svg
  9. // @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js
  10. // @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js
  11. // @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js
  12. // @resource     PRISM_CSS https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css
  13. // @resource     PRISM_LINE_NUMBERS_CSS https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css
  14. // @grant        GM_addStyle
  15. // @grant        GM_getResourceText
  16. // @grant        GM_setClipboard
  17. // ==/UserScript==
  18.  
  19. (function() {
  20.     'use strict';
  21.     console.log('AI Studio Chat Enhancements v0.9.21 LOADED (Scroll Padding on Nav)');
  22.  
  23.     const NAV_SCROLL_PADDING = 60; // Pixels for top/bottom padding when navigating
  24.  
  25.     const prismCss = GM_getResourceText("PRISM_CSS");
  26.     const prismLineNumbersCss = GM_getResourceText("PRISM_LINE_NUMBERS_CSS");
  27.     GM_addStyle(prismCss); GM_addStyle(prismLineNumbersCss);
  28.     GM_addStyle(`
  29.         /* Styles from v0.9.17 (and common parts from v0.9.15) */
  30.         .chat-nav-btn, .extract-code-blocks-btn, .toolbar-nav-btn-wrapper > button { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; margin-left: 0px; border: none; background-color: transparent; border-radius: 50%; width: 28px; height: 28px; line-height: 28px; }
  31.         .chat-nav-btn { margin-left: 4px !important; } .extract-code-blocks-btn { margin-left: 4px !important; }
  32.         .chat-nav-btn, .extract-code-blocks-btn { width: 30px !important; height: 30px !important; line-height: 30px !important; }
  33.         .toolbar-nav-btn-wrapper { display: inline-flex; margin: 0 2px; }
  34.         .toolbar-nav-btn-wrapper > button { width: 32px; height: 32px; line-height:32px; margin-left: 0px !important; }
  35.         .chat-nav-btn:hover, .extract-code-blocks-btn:hover, .toolbar-nav-btn-wrapper > button:hover { background-color: rgba(255, 255, 255, 0.15); }
  36.         .chat-nav-btn .material-symbols-outlined, .extract-code-blocks-btn .material-symbols-outlined, .toolbar-nav-btn-wrapper > button .material-symbols-outlined { font-family: 'Material Symbols Outlined', 'Material Icons'; font-size: 20px !important; font-weight: normal; font-style: normal; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; vertical-align: middle; color: #FFFFFF !important; }
  37.         .toolbar-nav-btn-wrapper > button .material-symbols-outlined { font-size: 22px !important; }
  38.         .code-display-modal {position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s;}
  39.         .code-display-modal.visible { opacity: 1; visibility: visible; }
  40.         .code-display-modal-content {background-color: #272822; padding: 20px; border-radius: 8px; width: 80%; max-width: 1000px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0,0,0,0.3);}
  41.         .code-display-modal-header {display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;}
  42.         .code-display-modal-header h3 { margin: 0; color: #f8f8f2; font-size: 1.2em; }
  43.         .code-display-modal-header .modal-controls { display: flex; align-items: center; }
  44.         .code-display-modal-header .modal-close-btn, .code-display-modal-header .modal-copy-btn {background: #555; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-left: 10px;}
  45.         .code-display-modal-header .modal-close-btn:hover, .code-display-modal-header .modal-copy-btn:hover { background: #777; }
  46.         .code-block-selector { margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #555; }
  47.         .code-block-selector label { color: #f8f8f2; margin-right: 10px; }
  48.         .code-block-selector select { padding: 5px; background: #333; color: white; border: 1px solid #555; border-radius: 4px; max-width: calc(100% - 120px); }
  49.         .code-display-modal-body { overflow: auto; flex-grow: 1; }
  50.         .code-display-modal-body pre[class*="language-"].line-numbers { position: relative; padding-left: 3.8em; counter-reset: linenumber; }
  51.         .code-display-modal-body pre[class*="language-"] { margin: 0; white-space: pre-wrap; word-break: break-all; }
  52.         .code-display-modal-body pre code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; font-size: 0.9em !important;}
  53.  
  54.         .gm-highlighted-turn {
  55.             outline: 2px solid #8ab4f8 !important;
  56.             outline-offset: 2px !important;
  57.             transition: outline 0.1s ease-in-out, outline-offset 0.1s ease-in-out;
  58.             border-radius: 4px;
  59.         }
  60.     `);
  61.  
  62.     let chatTurnObserver = null;
  63.     const CHAT_SESSION_SELECTOR = 'ms-chat-session';
  64.     const CHAT_TURN_SELECTOR = 'ms-chat-turn';
  65.     let codeModal, modalCodeElement, modalPreElement, modalCopyBtn, modalBlockSelector, currentCodeBlocks = [], currentCodeToCopyForModal = '';
  66.  
  67.     if (window.Prism) { Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/'; }
  68.  
  69.     // --- Code Viewer Functions (Unchanged from v0.9.20 corrected) ---
  70.     function createModal() { /* ... */ }
  71.     function displaySelectedBlockInModal(index) { /* ... */ }
  72.     function showCodeModalForTurn(blocks) { /* ... */ }
  73.     function hideCodeModal() { /* ... */ }
  74.     function getDescriptiveNameForCode(codeContent, lang) { /* ... */ }
  75.     function extractCodeBlocksFromTurn(chatTurnElement) { /* ... */ }
  76.     function addExtractCodeButtonToTurn(chatTurnElement) { /* ... */ }
  77.     // (Full definitions for these functions are included in the thought block, keeping them here for brevity)
  78.     // START: Code Viewer Functions (Copied from previous correct version)
  79.     function createModal() {
  80.         if (document.getElementById('customCodeDisplayModal')) return;
  81.         codeModal = document.createElement('div'); codeModal.id = 'customCodeDisplayModal'; codeModal.classList.add('code-display-modal');
  82.         const content = document.createElement('div'); content.classList.add('code-display-modal-content');
  83.         const header = document.createElement('div'); header.classList.add('code-display-modal-header');
  84.         const title = document.createElement('h3'); title.textContent = 'Code Block Viewer';
  85.         const controlsDiv = document.createElement('div'); controlsDiv.classList.add('modal-controls');
  86.         modalCopyBtn = document.createElement('button'); modalCopyBtn.textContent = 'Copy Code'; modalCopyBtn.classList.add('modal-copy-btn');
  87.         modalCopyBtn.onclick = () => { GM_setClipboard(currentCodeToCopyForModal); modalCopyBtn.textContent = 'Copied!'; setTimeout(() => { modalCopyBtn.textContent = 'Copy Code'; }, 2000); };
  88.         const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; closeBtn.classList.add('modal-close-btn'); closeBtn.onclick = hideCodeModal;
  89.         controlsDiv.appendChild(modalCopyBtn); controlsDiv.appendChild(closeBtn); header.appendChild(title); header.appendChild(controlsDiv);
  90.         modalBlockSelector = document.createElement('div'); modalBlockSelector.classList.add('code-block-selector'); modalBlockSelector.style.display = 'none';
  91.         const body = document.createElement('div'); body.classList.add('code-display-modal-body');
  92.         modalPreElement = document.createElement('pre'); modalPreElement.classList.add('line-numbers'); modalCodeElement = document.createElement('code'); modalPreElement.appendChild(modalCodeElement); body.appendChild(modalPreElement);
  93.         content.appendChild(header); content.appendChild(modalBlockSelector); content.appendChild(body); codeModal.appendChild(content); document.body.appendChild(codeModal);
  94.         document.addEventListener('keydown', (e) => { if (e.key === "Escape" && codeModal.classList.contains('visible')) hideCodeModal(); });
  95.         codeModal.addEventListener('click', (e) => { if (e.target === codeModal) hideCodeModal(); });
  96.     }
  97.     function displaySelectedBlockInModal(index) {
  98.         if (!codeModal || index < 0 || index >= currentCodeBlocks.length) return;
  99.         const block = currentCodeBlocks[index]; currentCodeToCopyForModal = block.content;
  100.         const modalBody = codeModal.querySelector('.code-display-modal-body'); if (modalBody) { modalBody.scrollTop = 0; }
  101.         modalCodeElement.textContent = block.content; modalCodeElement.className = ''; modalPreElement.className = 'line-numbers';
  102.         if (block.lang && block.lang !== 'plain') { const langClass = `language-${block.lang.toLowerCase()}`; modalCodeElement.classList.add(langClass); modalPreElement.classList.add(langClass);
  103.         } else { modalCodeElement.classList.add('language-none'); modalPreElement.classList.add('language-none'); }
  104.         Prism.highlightElement(modalCodeElement);
  105.     }
  106.     function showCodeModalForTurn(blocks) {
  107.         if (!codeModal) createModal(); currentCodeBlocks = blocks;
  108.         if (blocks.length === 0) { modalCodeElement.textContent = "No code blocks found in this message."; modalPreElement.className = 'line-numbers language-none'; modalCodeElement.className = 'language-none'; currentCodeToCopyForModal = ""; modalBlockSelector.style.display = 'none'; modalCopyBtn.style.display = 'none'; const modalBody = codeModal.querySelector('.code-display-modal-body'); if (modalBody) modalBody.scrollTop = 0; Prism.highlightElement(modalCodeElement);
  109.         } else if (blocks.length === 1) { modalBlockSelector.style.display = 'none'; modalCopyBtn.style.display = 'inline-block'; displaySelectedBlockInModal(0);
  110.         } else {
  111.             modalCopyBtn.style.display = 'inline-block'; modalBlockSelector.innerHTML = ''; const label = document.createElement('label'); label.textContent = 'Select Code Block:'; const select = document.createElement('select');
  112.             let longestBlockIndex = 0; if (blocks.length > 0) { let maxLines = -1; blocks.forEach((block, index) => { const numLines = block.content.split('\n').length; if (numLines > maxLines) { maxLines = numLines; longestBlockIndex = index; } const option = document.createElement('option'); option.value = index; const displayName = block.name || `Block ${index + 1}`; option.textContent = `${displayName} (${block.lang || 'plain'}, ${numLines} lines)`; select.appendChild(option); }); }
  113.             select.value = longestBlockIndex; select.onchange = (e) => displaySelectedBlockInModal(parseInt(e.target.value));
  114.             modalBlockSelector.appendChild(label); modalBlockSelector.appendChild(select); modalBlockSelector.style.display = 'block'; displaySelectedBlockInModal(longestBlockIndex);
  115.         } codeModal.classList.add('visible'); document.body.style.overflow = 'hidden';
  116.     }
  117.     function hideCodeModal() {
  118.         if (codeModal) codeModal.classList.remove('visible'); document.body.style.overflow = '';
  119.     }
  120.     function getDescriptiveNameForCode(codeContent, lang) {
  121.         const firstFewLines = codeContent.substring(0, 500).split('\n'); let name = null; if (lang === 'javascript' || lang === 'js') { const nameMatch = codeContent.match(/\/\/\s*@name\s+(.*)/); if (nameMatch && nameMatch[1]) return nameMatch[1].trim(); } for (const line of firstFewLines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; if (lang === 'python' || lang === 'py') { let match = trimmedLine.match(/^def\s+([a-zA-Z_][\w]*)/) || trimmedLine.match(/^class\s+([a-zA-Z_][\w]*)/); if (match && match[1]) { name = match[1]; break; } } else if (lang === 'javascript' || lang === 'js') { let match = trimmedLine.match(/^function\s+([a-zA-Z_$][\w$]*)/) || trimmedLine.match(/^(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*function/) || trimmedLine.match(/^class\s+([a-zA-Z_$][\w$]*)/); if (match && match[1]) { name = match[1]; break; } } else if (lang === 'html' || lang === 'xml') { let match = trimmedLine.match(/<title[^>]*>\s*(.*?)\s*<\/title>/i) || trimmedLine.match(/<h1[^>]*>\s*(.*?)\s*<\/h1>/i); if (match && match[1]) { name = match[1].trim(); break; } } if (!name && !trimmedLine.startsWith('//') && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('/*') && !trimmedLine.startsWith('<!--')) { name = trimmedLine.substring(0, 30) + (trimmedLine.length > 30 ? "..." : ""); break; } } return name;
  122.     }
  123.     function extractCodeBlocksFromTurn(chatTurnElement) {
  124.         const blocks = []; const renderedPreElements = chatTurnElement.querySelectorAll('.model-prompt-container .turn-content pre'); renderedPreElements.forEach((pre) => { const codeElement = pre.querySelector('code'); if (codeElement) { let lang = 'plain'; for (const cls of codeElement.classList) { if (cls.startsWith('language-') && cls.length > 'language-'.length) { lang = cls.substring('language-'.length); break; } if (Prism.languages[cls]) { if (lang === 'plain' || !codeElement.className.includes('language-')) { lang = cls; if (!codeElement.className.includes('language-')) break;} } } if (lang === 'plain') { for (const cls of pre.classList) { if (cls.startsWith('language-') && cls.length > 'language-'.length) { lang = cls.substring('language-'.length); break; } if (Prism.languages[cls]) { if (lang === 'plain' || !pre.className.includes('language-')) {lang = cls; if (!pre.className.includes('language-')) break;} } } } if (lang === 'plain' && pre.dataset.lang) { lang = pre.dataset.lang; } const content = codeElement.innerText.trim(); const descriptiveName = getDescriptiveNameForCode(content, lang.toLowerCase()); blocks.push({ lang: lang.toLowerCase(), content: content, source: 'rendered', name: descriptiveName }); } }); if (blocks.length === 0) { const textChunkElements = chatTurnElement.querySelectorAll('.model-prompt-container .turn-content ms-text-chunk'); let combinedRawMarkdown = ""; if (textChunkElements.length > 0) { textChunkElements.forEach((chunk) => { const cmarkChild = chunk.querySelector('ms-cmark-node'); let contentSource = cmarkChild && chunk.children.length === 1 && cmarkChild.matches('ms-cmark-node') ? cmarkChild : chunk; combinedRawMarkdown += contentSource.textContent + "\n\n"; }); combinedRawMarkdown = combinedRawMarkdown.trim(); } if (combinedRawMarkdown) { const codeBlockRegex = /(?:^|\n)\s*```(\w*)?\s*?\n?([\s\S]*?)\n?\s*```\s*(?:\n|$)/gm; let match; while ((match = codeBlockRegex.exec(combinedRawMarkdown)) !== null) { const langRegex = match[1] || 'plain'; let content = match[2].trim(); const descriptiveName = getDescriptiveNameForCode(content, langRegex.toLowerCase()); blocks.push({ lang: langRegex.toLowerCase(), content: content, source: 'raw_parsed', name: descriptiveName }); } } } return blocks;
  125.     }
  126.     function addExtractCodeButtonToTurn(chatTurnElement) {
  127.         if (!chatTurnElement.querySelector('.model-prompt-container')) return; const actionsContainer = chatTurnElement.querySelector('.actions-container .actions.hover-or-edit'); if (actionsContainer && !actionsContainer.querySelector('.extract-code-blocks-btn')) { const extractBtn = document.createElement('button'); extractBtn.classList.add('extract-code-blocks-btn'); extractBtn.title = 'View/Copy Code Blocks'; const iconSpan = document.createElement('span'); iconSpan.classList.add('material-symbols-outlined', 'notranslate'); iconSpan.setAttribute('aria-hidden', 'true'); iconSpan.textContent = 'code_blocks'; extractBtn.appendChild(iconSpan); extractBtn.addEventListener('click', (e) => { e.stopPropagation(); const foundBlocks = extractCodeBlocksFromTurn(chatTurnElement); showCodeModalForTurn(foundBlocks); }); actionsContainer.prepend(extractBtn); }
  128.     }
  129.     // END: Code Viewer Functions
  130.  
  131.  
  132.     let lastInteractedTurn = null;
  133.     function findVisibleTurn() { /* ... (Unchanged) ... */
  134.         const turns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR)); if (turns.length === 0) return null; const viewportHeight = window.innerHeight; let bestMatch = null; let smallestDistance = Infinity; for (const turn of turns) { const rect = turn.getBoundingClientRect(); if (rect.bottom > 0 && rect.top < viewportHeight) { const distanceToCenter = Math.abs(rect.top + rect.height / 2 - viewportHeight / 2); if (bestMatch === null || distanceToCenter < smallestDistance) { smallestDistance = distanceToCenter; bestMatch = turn; } } } return bestMatch || (turns.length > 0 ? turns[0] : null);
  135.     }
  136.  
  137.     // --- Helper function to find the scrollable parent ---
  138.     function getScrollableParent(element) {
  139.         let el = element;
  140.         while (el && el !== document.body && el !== document.documentElement) {
  141.             const overflowY = window.getComputedStyle(el).overflowY;
  142.             if (overflowY === 'auto' || overflowY === 'scroll') {
  143.                 if (el.scrollHeight > el.clientHeight) { // Check if it's actually scrollable
  144.                     return el;
  145.                 }
  146.             }
  147.             el = el.parentElement;
  148.         }
  149.         return window; // Fallback to window if no specific scrollable parent is found
  150.     }
  151.  
  152.     // --- MODIFIED addGlobalPrevNextButtons function (incorporates Smart Next, Highlight, and Scroll Padding) ---
  153.     function addGlobalPrevNextButtons() {
  154.         const toolbarContainer = document.querySelector('ms-toolbar > div.toolbar-container');
  155.         if (toolbarContainer && !toolbarContainer.querySelector('button.global-prev-msg-btn')) {
  156.             const createToolbarButton = (icon, title, direction) => {
  157.                 const wrapperDiv = document.createElement('div');
  158.                 wrapperDiv.classList.add('toolbar-nav-btn-wrapper');
  159.                 const btn = document.createElement('button');
  160.                 btn.classList.add('mdc-icon-button', 'mat-mdc-icon-button', 'mat-mdc-button-base', 'gmat-mdc-button', 'mat-unthemed');
  161.                 btn.setAttribute('aria-label', title);
  162.                 btn.title = title;
  163.                 const spanRipple = document.createElement('span');
  164.                 spanRipple.className = 'mat-mdc-button-persistent-ripple mdc-icon-button__ripple';
  165.                 btn.appendChild(spanRipple);
  166.                 const spanIcon = document.createElement('span');
  167.                 spanIcon.className = 'material-symbols-outlined notranslate';
  168.                 spanIcon.setAttribute('aria-hidden', 'true');
  169.                 spanIcon.textContent = icon;
  170.                 btn.appendChild(spanIcon);
  171.                 const spanFocus = document.createElement('span');
  172.                 spanFocus.className = 'mat-focus-indicator';
  173.                 btn.appendChild(spanFocus);
  174.                 const spanTouch = document.createElement('span');
  175.                 spanTouch.className = 'mat-mdc-button-touch-target';
  176.                 btn.appendChild(spanTouch);
  177.                 wrapperDiv.appendChild(btn);
  178.  
  179.                 btn.addEventListener('click', () => {
  180.                     const allTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR));
  181.                     if (allTurns.length === 0) return;
  182.  
  183.                     let currentTurnForNav = lastInteractedTurn || findVisibleTurn();
  184.                     if (!currentTurnForNav || !allTurns.includes(currentTurnForNav)) {
  185.                         currentTurnForNav = findVisibleTurn();
  186.                     }
  187.                     if (!currentTurnForNav) return;
  188.  
  189.                     let currentIndex = allTurns.indexOf(currentTurnForNav);
  190.                     let targetTurn = null;
  191.                     let scrollBlockPosition = 'start';
  192.                     let scrollAdjustment = -NAV_SCROLL_PADDING; // For 'start', scroll window UP to push element DOWN
  193.  
  194.                     if (direction === 'previous') {
  195.                         if (currentIndex > 0) {
  196.                             targetTurn = allTurns[currentIndex - 1];
  197.                         } else {
  198.                             targetTurn = allTurns[0];
  199.                         }
  200.                         // scrollBlockPosition = 'start', scrollAdjustment = -NAV_SCROLL_PADDING
  201.                     } else if (direction === 'next') {
  202.                         if (currentIndex === allTurns.length - 1) {
  203.                             targetTurn = allTurns[currentIndex];
  204.                             scrollBlockPosition = 'end';
  205.                             scrollAdjustment = NAV_SCROLL_PADDING; // For 'end', scroll window DOWN to push element UP
  206.                         } else {
  207.                             targetTurn = allTurns[currentIndex + 1];
  208.                             // scrollBlockPosition = 'start', scrollAdjustment = -NAV_SCROLL_PADDING
  209.                         }
  210.                     }
  211.  
  212.                     if (targetTurn) {
  213.                         allTurns.forEach(turn => turn.classList.remove('gm-highlighted-turn'));
  214.  
  215.                         targetTurn.scrollIntoView({ behavior: 'auto', block: scrollBlockPosition });
  216.  
  217.                         // Apply padding scroll adjustment to the correct scrollable container
  218.                         const scrollableParentToAdjust = getScrollableParent(targetTurn);
  219.                         if (scrollableParentToAdjust === window || scrollableParentToAdjust === document.documentElement || scrollableParentToAdjust === document.body) {
  220.                             window.scrollBy(0, scrollAdjustment);
  221.                         } else if (scrollableParentToAdjust && typeof scrollableParentToAdjust.scrollBy === 'function') {
  222.                             scrollableParentToAdjust.scrollBy(0, scrollAdjustment);
  223.                         }
  224.  
  225.                         lastInteractedTurn = targetTurn;
  226.  
  227.                         targetTurn.classList.add('gm-highlighted-turn');
  228.                         if (targetTurn.gmHighlightTimeout) {
  229.                             clearTimeout(targetTurn.gmHighlightTimeout);
  230.                         }
  231.                         targetTurn.gmHighlightTimeout = setTimeout(() => {
  232.                             targetTurn.classList.remove('gm-highlighted-turn');
  233.                             delete targetTurn.gmHighlightTimeout;
  234.                         }, 1500);
  235.                     }
  236.                 });
  237.                 return wrapperDiv;
  238.             };
  239.  
  240.             const prevButtonWrapper = createToolbarButton('keyboard_arrow_up', 'Previous Message', 'previous');
  241.             prevButtonWrapper.querySelector('button').classList.add('global-prev-msg-btn');
  242.             const nextButtonWrapper = createToolbarButton('keyboard_arrow_down', 'Next Message', 'next');
  243.             nextButtonWrapper.querySelector('button').classList.add('global-next-msg-btn');
  244.  
  245.             const compareButtonWrapper = toolbarContainer.querySelector('div.compare-button');
  246.             if (compareButtonWrapper) {
  247.                 toolbarContainer.insertBefore(prevButtonWrapper, compareButtonWrapper);
  248.                 toolbarContainer.insertBefore(nextButtonWrapper, compareButtonWrapper);
  249.             } else {
  250.                 const clearChatButtonWrapper = toolbarContainer.querySelector('div[mattooltip="Clear chat"]');
  251.                 if (clearChatButtonWrapper) {
  252.                     toolbarContainer.insertBefore(prevButtonWrapper, clearChatButtonWrapper);
  253.                     toolbarContainer.insertBefore(nextButtonWrapper, clearChatButtonWrapper);
  254.                 } else {
  255.                     toolbarContainer.appendChild(prevButtonWrapper);
  256.                     toolbarContainer.appendChild(nextButtonWrapper);
  257.                 }
  258.             }
  259.         }
  260.     }
  261.     // --- END MODIFIED addGlobalPrevNextButtons function ---
  262.  
  263.  
  264.     // --- REVERTED createScrollInTurnButton (Unchanged) ---
  265.     function createScrollInTurnButton(iconName, title, currentTurnElement, targetPos) { /* ... */
  266.         const button = document.createElement('button');
  267.         button.classList.add('chat-nav-btn');
  268.         button.title = title;
  269.         const iconSpan = document.createElement('span');
  270.         iconSpan.classList.add('material-symbols-outlined', 'notranslate');
  271.         iconSpan.setAttribute('aria-hidden', 'true');
  272.         iconSpan.textContent = iconName;
  273.         button.appendChild(iconSpan);
  274.  
  275.         button.addEventListener('click', (event) => {
  276.             event.stopPropagation();
  277.             currentTurnElement.scrollIntoView({ behavior: 'auto', block: targetPos });
  278.         });
  279.         return button;
  280.     }
  281.  
  282.     // --- addScrollInTurnButtons (Unchanged) ---
  283.     function addScrollInTurnButtons(chatTurnElement) { /* ... */
  284.         const actionsContainer = chatTurnElement.querySelector('.actions-container .actions.hover-or-edit');
  285.         if (actionsContainer && !actionsContainer.querySelector('.scroll-to-turn-top-btn')) {
  286.             const scrollToTopBtn = createScrollInTurnButton('arrow_upward', 'Scroll to top of this message', chatTurnElement, 'start');
  287.             scrollToTopBtn.classList.add('scroll-to-turn-top-btn');
  288.             const scrollToBottomBtn = createScrollInTurnButton('arrow_downward', 'Scroll to bottom of this message', chatTurnElement, 'end');
  289.             scrollToBottomBtn.classList.add('scroll-to-turn-bottom-btn');
  290.             actionsContainer.prepend(scrollToBottomBtn);
  291.             actionsContainer.prepend(scrollToTopBtn);
  292.         }
  293.     }
  294.  
  295.     // --- initializeChatTurnObserver (Unchanged) ---
  296.     function initializeChatTurnObserver(chatSessionElement) { /* ... */
  297.         if (chatTurnObserver) chatTurnObserver.disconnect();
  298.         const processAllInTurn = (turn) => {
  299.             addScrollInTurnButtons(turn);
  300.             addExtractCodeButtonToTurn(turn);
  301.             turn.addEventListener('click', () => { lastInteractedTurn = turn; }, {capture: true});
  302.         };
  303.         chatSessionElement.querySelectorAll(CHAT_TURN_SELECTOR).forEach(processAllInTurn);
  304.         chatTurnObserver = new MutationObserver(mutations => {
  305.             mutations.forEach(mutation => {
  306.                 mutation.addedNodes.forEach(node => {
  307.                     if (node.nodeType === Node.ELEMENT_NODE) {
  308.                         if (node.matches && node.matches(CHAT_TURN_SELECTOR)) { processAllInTurn(node); }
  309.                         else { node.querySelectorAll(CHAT_TURN_SELECTOR).forEach(processAllInTurn); }
  310.                     }
  311.                 });
  312.             });
  313.         });
  314.         chatTurnObserver.observe(chatSessionElement, { childList: true, subtree: true });
  315.     }
  316.  
  317.     // --- mainAppObserver and Initial Setup (Unchanged) ---
  318.     const mainAppObserver = new MutationObserver(() => { /* ... */
  319.         const chatSession = document.querySelector(CHAT_SESSION_SELECTOR);
  320.         if (chatSession) {
  321.             if (!chatSession.dataset.enhancementsInitialized) {
  322.                 initializeChatTurnObserver(chatSession);
  323.                 addGlobalPrevNextButtons();
  324.                 chatSession.dataset.enhancementsInitialized = 'true';
  325.             }
  326.         } else {
  327.             if (chatTurnObserver) { chatTurnObserver.disconnect(); chatTurnObserver = null; }
  328.             const oldSession = document.querySelector(`[data-enhancements-initialized="true"]`);
  329.             if (oldSession) delete oldSession.dataset.enhancementsInitialized;
  330.         }
  331.     });
  332.  
  333.     mainAppObserver.observe(document.body, { childList: true, subtree: true });
  334.     const initialChatSession = document.querySelector(CHAT_SESSION_SELECTOR);
  335.     if (initialChatSession) {
  336.         initializeChatTurnObserver(initialChatSession);
  337.         addGlobalPrevNextButtons();
  338.         initialChatSession.dataset.enhancementsInitialized = 'true';
  339.     }
  340.     createModal();
  341. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement