Interactive Sidebar Navigator for ChatGPT

A user-friendly, interactive sidebar for the ChatGPT official website that does not cover the header, footer, or the scrollbar.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Interactive Sidebar Navigator for ChatGPT
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  A user-friendly, interactive sidebar for the ChatGPT official website that does not cover the header, footer, or the scrollbar.
// @description:zh-CN  为ChatGPT官网提供了不遮挡页眉、页脚或滚动条的用户友好、交互性强的侧边栏。
// @license      GPL-3.0-or-later
// @match        https://chat.openai.com/**
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Define the header and footer heights if known, or estimate
    const headerHeight = '60px'; // Change this value to the actual height of your header
    const footerHeight = '60px'; // Change this value to the actual height of your footer

    // Insert custom styles into the document
    const style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = `
        #customSidebar {
            position: fixed;
            top: ${headerHeight};
            bottom: ${footerHeight};
            right: 0;
            width: 250px;
            background-color: #000;
            color: #fff;
            overflow-y: auto;
            overflow-x: hidden;
            transition: transform 0.3s ease-out;
            transform: translateX(250px);
            z-index: 9999;
            box-shadow: -2px 0 5px rgba(0,0,0,0.5);
            padding-bottom: 10px; // Adjusted padding for footer
        }

        #customSidebar div {
            padding: 10px;
            border-bottom: 1px solid #444;
            text-overflow: ellipsis;
            overflow: hidden;
            white-space: nowrap;
            cursor: pointer;
        }

        #customSidebar div.active {
            background-color: #777; // Highlight active item
        }

        /* Tooltip styles */
        .sidebar-tooltip {
            visibility: hidden;
            background-color: #555;
            color: #fff;
            text-align: left;
            border-radius: 5px;
            padding: 5px;
            position: absolute;
            z-index: 10001;
            left: 100%;
            top: 0;
            white-space: pre-wrap;
            word-wrap: break-word;
            opacity: 0;
            transition: visibility 0s, opacity 0.5s linear;
        }

        .tooltip {
            visibility: hidden;
            background-color: #555;
            color: #fff;
            text-align: left;
            border-radius: 5px;
            padding: 5px;
            position: absolute;
            z-index: 10001;
            left: 100%;
            margin-left: 10px; // Space between item and tooltip
            white-space: nowrap;
            word-wrap: break-word;
            transition: visibility 0s linear 0.3s, opacity 0.3s linear 0.3s;
            opacity: 0;
            pointer-events: none; // Tooltip should not interfere with mouse events
        }

        .sidebar-item:hover .tooltip {
            visibility: visible;
            opacity: 1;
            transition-delay: 0s;
        }

        .sidebar-tooltip-show {
            visibility: visible;
            opacity: 1;
            transition-delay: 3s; // Delay to show tooltip after 3s of hover
        }
        .sidebar-item:hover::after {
            visibility: visible;
            opacity: 1;
            transition-delay: 0s;
        }

        #tooltipContainer {
            display: none; // Start with the tooltip container not displayed
            position: fixed;
            z-index: 10001;
            pointer-events: none; // Ensure the tooltip does not interfere with mouse events
            transition: opacity 0.3s ease-in-out;
            opacity: 0;
        }

        .visible #tooltipContainer {
            display: block; // Display tooltip container when a tooltip is visible
            opacity: 1;
        }

        #sidebarToggle {
            position: fixed;
            right: 250px;
            top: calc(50% - 20px); // Center toggle vertically, adjusting for its own height
            transform: translateX(100%);
            z-index: 10000;
            cursor: pointer;
            background-color: #444;
            color: #fff;
            border: none;
            width: 30px;
            height: 40px;
            border-radius: 5px 0 0 5px;
            outline: none;
            transition: right 0.3s ease-out, transform 0.3s ease-out;
        }

        .sidebar-icon-bar {
            display: block;
            width: 20px;
            height: 2px;
            background-color: #fff;
            margin: 6px auto;
            transition: background-color 0.3s, transform 0.3s ease-out;
        }

        .toggle-open .top-bar {
            transform: translateY(8px) rotateZ(45deg);
        }

        .toggle-open .middle-bar {
            opacity: 0;
        }

        .toggle-open .bottom-bar {
            transform: translateY(-8px) rotateZ(-45deg);
        }

        body {
            padding-right: 250px; // Make space for the sidebar when it is expanded
        }
        div.sticky.top-0 {
          opacity: 0.3;
        }
    `;

  document.head.appendChild(style);

	let questionAnswerSelector = '.flex-col.gap-1.md\\:gap-3'
	let customSidebarSelector = '#customSidebar > div'
    let questionAnswerLength=2
    let customSidebarLlength=0

  function updateSidebarContent(){
      let elementWithAttribute = document.querySelector(questionAnswerSelector);

      if (elementWithAttribute) {
        sidebar.innerHTML = ''
        console.log('elementWithAttribute exists: update')
      }else{
        console.log('elementWithAttribute not exists')
        // updateSidebarContent()
      }
        const allTextItems = Array.from(document.querySelectorAll(questionAnswerSelector));
        if (allTextItems.length > 0) {
            // observer.disconnect();

            // Populate the sidebar with items
            allTextItems.forEach((item, index) => {
                if (index % 2 === 0) { // Add odd items
                    const div = document.createElement('div');
                    div.textContent = item.textContent.trim() || 'Untitled';
                  // createTooltip(div, div.textContent); // Add tooltip to each item
                    div.setAttribute('title', div.textContent); // Set the title for default browser tooltip



            // Setup mouse events for showing and hiding the tooltip
            let hoverTimeout;
            div.addEventListener('mouseenter', (e) => {
                const rect = div.getBoundingClientRect();
                hoverTimeout = setTimeout(() => {
                    showTooltip(div.textContent, rect.right, rect.top + window.scrollY);
                }, 3000);
            });
            div.addEventListener('mouseleave', () => {
                clearTimeout(hoverTimeout);
                hideTooltip();
            });

                    div.addEventListener('click', () => {
                        // Scroll to the element on the page
                        item.scrollIntoView({ behavior: 'smooth', block: 'start' });
                        // Highlight the active item
                        document.querySelectorAll('#customSidebar div').forEach(d => d.classList.remove('active'));
                        div.classList.add('active');
                    });
                    sidebar.appendChild(div);
                }

            // Adjust sidebar overflow after populating it
            adjustSidebarOverflow();
            });

            // Initially open the sidebar
            sidebar.style.transform = 'translateX(0)';
            toggleButton.classList.add('toggle-open');
            // Adjust the toggle button position
            setToggleButtonPosition();


            const customSidebar = document.getElementById('customSidebar');
            if (customSidebar) {
                const divs = customSidebar.querySelectorAll('div');
                divs.forEach((div, index) => {
                    div.textContent = `${index + 1}: ${div.textContent}`;
                });
            } else {
                console.log('#customSidebar not found');
            }

        }
  }


  // Create a global tooltip container
  const tooltipContainer = document.createElement('div');
  tooltipContainer.id = 'tooltipContainer';
  document.body.appendChild(tooltipContainer);


  // Function to update tooltip content and position
  function showTooltip(text, x, y) {
      tooltipContainer.textContent = text;
      tooltipContainer.style.top = `${y}px`;
      tooltipContainer.style.left = `${x}px`;
      tooltipContainer.classList.add('visible');
  }

  function hideTooltip() {
      tooltipContainer.classList.remove('visible');
  }

    // Create the sidebar element
    const sidebar = document.createElement('div');
    sidebar.id = 'customSidebar';
    document.body.appendChild(sidebar);

    // Create the toggle button
    const toggleButton = document.createElement('button');
    toggleButton.id = 'sidebarToggle';
    toggleButton.innerHTML = `
        <div class="sidebar-icon-bar top-bar"></div>
        <div class="sidebar-icon-bar middle-bar"></div>
        <div class="sidebar-icon-bar bottom-bar"></div>
    `;
    document.body.appendChild(toggleButton);

    // Function to set the correct position of the toggle button
    function setToggleButtonPosition() {
        const isSidebarVisible = sidebar.style.transform === 'translateX(0px)';
        toggleButton.style.right = isSidebarVisible ? '250px' : '0';
        toggleButton.style.transform = `translateX(${isSidebarVisible ? '-100%' : '0'})`;
    }

    // Initial call to set the toggle button position
    setToggleButtonPosition();

    // Toggle functionality
    toggleButton.addEventListener('click', function() {
        const isClosed = sidebar.style.transform.includes('250px');
        sidebar.style.transform = isClosed ? 'translateX(0)' : 'translateX(250px)';
        toggleButton.classList.toggle('toggle-open', isClosed);
        // Wait for the transition to finish before adjusting the toggle button position
        setTimeout(setToggleButtonPosition, 300);
    });


      // Adjust sidebar overflow based on its content height
    function adjustSidebarOverflow() {
        const sidebar = document.getElementById('customSidebar');
        if (sidebar.scrollHeight > sidebar.clientHeight) {
            sidebar.style.overflowY = 'auto';
        } else {
            sidebar.style.overflowY = 'hidden';
        }
    }

  // Observe mutations to the page content
  const observer = new MutationObserver(mutations => {
    questionAnswerLength = document.querySelectorAll(questionAnswerSelector).length/2
    customSidebarLlength = document.querySelectorAll(customSidebarSelector).length
    if (questionAnswerLength == customSidebarLlength){
      console.log(`questionAnswerLength: ${questionAnswerLength} == customSidebarLlength: ${customSidebarLlength}`)
      return
    }
    mutations.forEach(mutation => {
        // Check if the mutation occurs on a form element or its descendants
        let target = mutation.target;
        while (target !== document && target.nodeName !== 'FORM') {
            target = target.parentNode;
        }

        // If the change does not occur on a form element, perform the corresponding action
        if (target.nodeName !== 'FORM') {
            console.log('Performing changes on non-form element');
            console.log('Invoke updateSidebarContent');
            sidebar.innerHTML = '';
            updateSidebarContent();
        }
    });


  });

  // Configuration for the observer
  const config = {
      // attributes: true,       // Listen for attribute changes
      // characterData: true,    // Listen for text content changes
      childList: true,        // Listen for additions/removals of child elements
      subtree: true           // Listen for changes in all descendant nodes
  };


  let checkExist = setInterval(function() {
    // '.flex-col.gap-1.md\\:gap-3'
    // 'div.group.relative.active\\:opacity-90'
     // Find the element to observe (e.g., main content area)
    const observeElement = document.querySelector('main');
    if (observeElement != null) {
      console.log(`observeElement: ${observeElement}`);
      // Start observing mutations
      observer.observe(observeElement, config);
      clearInterval(checkExist);

    }
  }, 1000);


})();