Thủ Thuật

Hướng Dẫn Tạo Mục Lục Trong ChatGPT

Mọi người thường sử dụng ChatGPT để hỏi đáp trong lúc làm việc. Có khi câu hỏi nhiều và quá dài thì vấn đề kéo chuột tìm lại các câu trả lời thì rất dài và mất thời gian. Hôm nay hnitmedia sẽ hướng dẫn các bạn tạo mục lục các câu hỏi trong chatgpt để dễ đọc hơn

Các bạn tải extension TamplerMonkey trong chrome. Và mở extension lên

Sau khi mở extension lên và các bạn tạo script mới sau đó chèn đoạn script sau vào.

// ==UserScript==
// @name        ChatGPT Question Navigation Sidebar 2.0
// @namespace   npm/chatgpt-question-navigator
// @version     2.0
// @author      Pitroytech
// @description Lấy cảm hứng từ “ChatGPT Question Navigation sidebar” của okokdi, nhưng viết lại hoàn toàn bằng mã mới — siêu nhanh, mượt nhẹ, hiệu ứng đẹp vượt trội (Inspired by “ChatGPT Question Navigation sidebar” by okokdi, entirely rewritten with new code — ultra-fast, lightweight, with significantly improved, elegant effects.
// @match       https://chatgpt.com/**
// @grant       none
// ==/UserScript==


(function () {
  'use strict';

  // ====== CONFIG ======
  // tìm icon sidebar để thay <span class="toggle-btn">👁️</span>

  // Độ trong suốt khi sidebar ĐANG THU GỌN và KHÔNG hover
  // 0 = hoàn toàn trong suốt, 1 = đục hoàn toàn
  const COLLAPSED_OPACITY = 0.35;

  // Thời gian ép sidebar "dính cuối" sau khi khởi tạo (ms)
  // Gợi ý: 300–600ms (càng thấp thì càng ít can thiệp)
  const FORCE_STICK_BOTTOM_MS = 500;
  // =====================

  const DOM_MARK = 'data-chatgpt-question-directory';
  const CHAT_LIST_EL_CLASS = 'flex flex-col text-sm';
  const isSharePage = location.pathname.startsWith('/share/');
  const scrollMarginTop = 56;
  const RIGHT_OFFSET_PX = 16; // Sidebar luôn neo cách mép phải 16px
  const TOP_MIN_MARGIN = 20;  // chặn kéo vượt viền trên
  const BOTTOM_MIN_MARGIN = 20; // chặn kéo vượt viền dưới
  const DRAG_THRESHOLD_PX = 3; // ngưỡng di chuyển để coi là kéo

  // Helper functions
  function queryChatContainer() {
    const main = document.querySelector('main');
    return main?.querySelector('.' + CHAT_LIST_EL_CLASS.split(' ').join('.'));
  }

  function queryQuestionEls() {
    const container = queryChatContainer();
    if (!container) return [];

    return Array.from(container.children)
      .filter(child => child.hasAttribute('data-testid'))
      .filter((_, index) => index % 2 === 0);
  }

  function getQuestions() {
    const questionEls = queryQuestionEls();
    return questionEls.map(el => {
      const textEl = el.querySelector('[data-message-author-role]');
      return textEl?.innerText || '';
    }).filter(Boolean);
  }

  function getConversationIdByUrl() {
    const match = location.pathname.match(/\/c\/(.*)/);
    return match?.[1] || null;
  }

  // Create sidebar UI
  function createSidebar() {
    const sidebar = document.createElement('div');
    sidebar.setAttribute(DOM_MARK, '');
    sidebar.innerHTML = `
      <style>
        [${DOM_MARK}] {
          position: fixed;
          top: 10vh;
          right: ${RIGHT_OFFSET_PX}px;
          padding: 12px;
          border-radius: 8px;
          background: rgba(17, 24, 39, 0.95);
          backdrop-filter: blur(10px);
          border: 1px solid rgba(255, 255, 255, 0.1);
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
          transition: all 0.65s cubic-bezier(0.4, 0, 0.2, 1);
          z-index: 1000;
          max-width: 300px;
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          opacity: 1; /* Mặc định hiển thị rõ */
        }

        [${DOM_MARK}].dragging {
          transition: none !important;
          cursor: grabbing !important;
          user-select: none !important;
          opacity: 0.9 !important;
        }

        [${DOM_MARK}].dragging * {
          pointer-events: none !important;
          cursor: grabbing !important;
        }

        [${DOM_MARK}].collapsed {
          width: 55px;
          overflow: hidden;
        }

        /* Thu gọn + KHÔNG hover => trong suốt toàn bộ */
        [${DOM_MARK}].collapsed:not(.hovering) {
          opacity: ${COLLAPSED_OPACITY};
        }

        [${DOM_MARK}].collapsed .questions-list {
          opacity: 0.8;
          visibility: visible;
          transition: opacity 0.45s ease, visibility 0.45s ease;
        }

        [${DOM_MARK}]:not(.collapsed) .questions-list,
        [${DOM_MARK}].hovering:not(.dragging) .questions-list {
          opacity: 1;
          visibility: visible;
          transition: opacity 0.45s ease, visibility 0.45s ease;
        }

        [${DOM_MARK}].hovering:not(.dragging) {
          width: auto;
          max-width: 300px;
          overflow: visible;
          transition: all 0.65s cubic-bezier(0.4, 0, 0.2, 1);
          opacity: 1; /* Hover vào để xem nội dung rõ ràng */
        }

        [${DOM_MARK}] .header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          margin-bottom: 8px;
          color: #e5e7eb;
          font-weight: 600;
          font-size: 14px;
          white-space: nowrap;
          gap: 8px;
          cursor: default; /* không còn kéo bằng header */
          user-select: none;
        }

        [${DOM_MARK}] .header:hover {
          background: rgba(255, 255, 255, 0.05);
          margin: -4px -4px 4px -4px;
          padding: 4px;
          border-radius: 4px;
        }

        [${DOM_MARK}] .title {
          opacity: 1;
          transition: opacity 0.45s ease;
          pointer-events: none;
        }

        [${DOM_MARK}].collapsed:not(.hovering) .title {
          opacity: 0;
          width: 0;
          overflow: hidden;
        }

        [${DOM_MARK}] .toggle-btn {
          cursor: grab; /* chỉ kéo bằng nút 👁️ */
          opacity: 0.8;
          transition: all 0.45s ease;
          font-size: 20px;
          padding: 2px;
          filter: grayscale(0);
          flex-shrink: 0;
          margin-left: auto;
        }

        [${DOM_MARK}] .toggle-btn:hover {
          opacity: 1;
          transform: scale(1.1);
        }

        [${DOM_MARK}].collapsed .toggle-btn {
          opacity: 1;
          filter: grayscale(0);
        }

        [${DOM_MARK}] .questions-list {
          max-height: 60vh;
          overflow-y: auto;
          margin: 0;
          padding: 0;
          list-style: none;
        }

        [${DOM_MARK}] .questions-list::-webkit-scrollbar {
          width: 4px;
        }

        [${DOM_MARK}] .questions-list::-webkit-scrollbar-track {
          background: transparent;
        }

        [${DOM_MARK}] .questions-list::-webkit-scrollbar-thumb {
          background: rgba(255, 255, 255, 0.2);
          border-radius: 2px;
        }

        [${DOM_MARK}] .questions-list::-webkit-scrollbar-thumb:hover {
          background: rgba(255, 255, 255, 0.3);
        }

        [${DOM_MARK}] .question-item {
          padding: 6px 8px;
          margin: 2px 0;
          color: #9ca3af;
          font-size: 13px;
          cursor: pointer;
          border-radius: 4px;
          transition: all 0.32s ease;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          display: flex;
          align-items: flex-start;
          gap: 8px;
        }

        [${DOM_MARK}].collapsed:not(.hovering) .question-item {
          padding: 6px 4px;
        }

        [${DOM_MARK}] .question-item:hover {
          background: rgba(255, 255, 255, 0.05);
          color: #e5e7eb;
        }

        [${DOM_MARK}] .question-item.active {
          background: rgba(16, 185, 129, 0.2);
          color: #10b981;
          font-weight: 500;
        }

        [${DOM_MARK}] .question-number {
          flex-shrink: 0;
          font-weight: 600;
          color: #6b7280;
          min-width: 20px;
          text-align: left;
        }

        [${DOM_MARK}] .question-item.active .question-number {
          color: #10b981;
        }

        [${DOM_MARK}] .question-text {
          flex: 1;
          overflow: hidden;
          text-overflow: ellipsis;
          transition: opacity 0.45s ease;
        }

        [${DOM_MARK}].collapsed:not(.hovering) .question-text {
          opacity: 0;
          width: 0;
        }
      </style>

      <div class="header">
        <span class="title">📋 Questions</span>
        <span class="toggle-btn">✮⋆˙</span>
      </div>
      <ol class="questions-list"></ol>
    `;

    return sidebar;
  }

  // Chỉ kéo khi giữ nút 👁️ và chỉ thay đổi tọa độ Y (neo bên phải)
  function setupDragBehavior(sidebar) {
    const toggleBtn = sidebar.querySelector('.toggle-btn');
    let isPointerDown = false;
    let isDragging = false;
    let startY = 0;
    let startTop = 0;
    let ignoreNextClick = false;

    // Luôn neo bên phải
    sidebar.style.right = RIGHT_OFFSET_PX + 'px';
    sidebar.style.left = '';

    // Load vị trí top đã lưu (nếu có)
    const savedTop = localStorage.getItem('chatgpt-sidebar-top');
    if (savedTop) {
      sidebar.style.top = savedTop;
    }

    // Không dùng right cũ nữa
    try { localStorage.removeItem('chatgpt-sidebar-right'); } catch {}

    function onPointerDown(e) {
      // Chỉ cho phép kéo khi bấm lên nút 👁️
      if (e.target !== toggleBtn) return;

      isPointerDown = true;
      isDragging = false;
      startY = (e.touches ? e.touches[0].clientY : e.clientY);
      startTop = sidebar.getBoundingClientRect().top;

      // Lắng nghe di chuyển/thả trên document
      document.addEventListener('mousemove', onPointerMove);
      document.addEventListener('mouseup', onPointerUp);
      document.addEventListener('touchmove', onPointerMove, { passive: false });
      document.addEventListener('touchend', onPointerUp);
    }

    function onPointerMove(e) {
      if (!isPointerDown) return;

      const clientY = (e.touches ? e.touches[0].clientY : e.clientY);
      const deltaY = clientY - startY;

      if (!isDragging && Math.abs(deltaY) > DRAG_THRESHOLD_PX) {
        isDragging = true;
        sidebar.classList.add('dragging');
        sidebar.classList.remove('hovering');
      }

      if (!isDragging) return;

      // Tính top mới, chỉ theo trục Y
      const rect = sidebar.getBoundingClientRect();
      const minTop = TOP_MIN_MARGIN;
      const maxTop = window.innerHeight - rect.height - BOTTOM_MIN_MARGIN;

      let newTop = startTop + deltaY;
      newTop = Math.max(minTop, Math.min(newTop, maxTop));

      // Áp dụng vị trí
      sidebar.style.top = newTop + 'px';
      sidebar.style.right = RIGHT_OFFSET_PX + 'px';
      sidebar.style.left = '';

      e.preventDefault();
      e.stopPropagation();
    }

    function onPointerUp() {
      if (!isPointerDown) return;

      if (isDragging) {
        // Lưu tọa độ Y (top) và vẫn neo phải
        const rect = sidebar.getBoundingClientRect();
        localStorage.setItem('chatgpt-sidebar-top', rect.top + 'px');
        sidebar.style.right = RIGHT_OFFSET_PX + 'px';

        // Tránh click toggle ngay sau khi kéo
        ignoreNextClick = true;
        setTimeout(() => { ignoreNextClick = false; }, 0);
      }

      isPointerDown = false;
      isDragging = false;
      sidebar.classList.remove('dragging');

      document.removeEventListener('mousemove', onPointerMove);
      document.removeEventListener('mouseup', onPointerUp);
      document.removeEventListener('touchmove', onPointerMove);
      document.removeEventListener('touchend', onPointerUp);
    }

    // Click nút 👁️: toggle (trừ khi vừa kéo)
    function onToggleClick(e) {
      if (ignoreNextClick) {
        e.preventDefault();
        e.stopPropagation();
        return;
      }
      e.stopPropagation();
      toggleSidebar(sidebar);
    }

    // Gắn events
    toggleBtn.addEventListener('mousedown', onPointerDown);
    toggleBtn.addEventListener('touchstart', onPointerDown, { passive: true });
    toggleBtn.addEventListener('click', onToggleClick);

    // Cleanup function
    sidebar._cleanupDrag = () => {
      toggleBtn.removeEventListener('mousedown', onPointerDown);
      toggleBtn.removeEventListener('touchstart', onPointerDown);
      toggleBtn.removeEventListener('click', onToggleClick);
      document.removeEventListener('mousemove', onPointerMove);
      document.removeEventListener('mouseup', onPointerUp);
      document.removeEventListener('touchmove', onPointerMove);
      document.removeEventListener('touchend', onPointerUp);
    };
  }

  // Scroll sidebar list to bottom
  function scrollSidebarToBottom(sidebar) {
    const list = sidebar.querySelector('.questions-list');
    if (list) {
      list.scrollTop = list.scrollHeight;
      setTimeout(() => {
        list.scrollTop = list.scrollHeight;
      }, 100);
    }
  }

  // Set specific question as active
  // scrollChat: true => cuộn KHUNG CHAT tới câu hỏi đó
  // scrollChat: false => KHÔNG đụng tới khung chat (chỉ highlight)
  function setActiveQuestion(sidebar, index, questionEls, opts = {}) {
    const { scrollChat = true } = opts;
    sidebar.querySelectorAll('.question-item').forEach((item, i) => {
      item.classList.toggle('active', i === index);
    });

    if (scrollChat && questionEls[index]) {
      setTimeout(() => {
        questionEls[index].scrollIntoView({ behavior: 'smooth', block: 'start' });
      }, 100);
    }
  }

  // Update active question based on scroll (không cuộn chat)
  function updateActiveQuestion(sidebar, questionEls) {
    const topThreshold = isSharePage ? scrollMarginTop : 0;
    let activeIndex = -1;

    for (let i = 0; i < questionEls.length; i++) {
      const rect = questionEls[i].getBoundingClientRect();
      if (rect.top >= topThreshold) {
        activeIndex = i;
        break;
      }
    }

    sidebar.querySelectorAll('.question-item').forEach((item, index) => {
      item.classList.toggle('active', index === activeIndex);
    });
  }

  // Update questions list (đảm bảo dính cuối nếu cần, và KHÔNG cuộn khung chat khi khởi tạo)
  function updateQuestionsList(sidebar, scrollToLast = false) {
    const list = sidebar.querySelector('.questions-list');
    if (!list) return;

    const questions = getQuestions();
    const questionEls = queryQuestionEls();

    // Lưu vị trí cuộn trước khi rebuild
    const prevScrollTop = list.scrollTop;
    const prevAtBottom = (list.scrollTop + list.clientHeight >= list.scrollHeight - 5);

    // Rebuild
    list.innerHTML = questions.map((q, index) => `
      <li class="question-item" data-index="${index}">
        <span class="question-number">${index + 1}.</span>
        <span class="question-text">${q}</span>
      </li>
    `).join('');

    const forceWindow = Date.now() < (sidebar._forceBottomUntil || 0);
    const shouldStick = scrollToLast || forceWindow || sidebar._stickToBottom || prevAtBottom;

    if (questions.length > 0 && shouldStick) {
      // Quan trọng: KHÔNG cuộn khung chat ở nhánh này
      setActiveQuestion(sidebar, questions.length - 1, questionEls, { scrollChat: false });
      requestAnimationFrame(() => scrollSidebarToBottom(sidebar));
    } else {
      updateActiveQuestion(sidebar, questionEls);
      // Khôi phục vị trí cuộn nếu không dính cuối
      requestAnimationFrame(() => { list.scrollTop = prevScrollTop; });
    }
  }

  // Handle click on question (click mới cuộn khung chat)
  function handleQuestionClick(e, sidebar) {
    const item = e.target.closest('.question-item');
    if (!item) return;

    const index = parseInt(item.dataset.index);
    const questionEls = queryQuestionEls();

    // Click => cuộn khung chat tới câu hỏi đó + set active
    setActiveQuestion(sidebar, index, questionEls, { scrollChat: true });
  }

  // Toggle sidebar collapse
  function toggleSidebar(sidebar) {
    const isCollapsed = sidebar.classList.contains('collapsed');
    sidebar.classList.toggle('collapsed');
    sidebar.classList.remove('hovering');
    sidebar._isManuallyToggled = true;
    sidebar._isOpen = isCollapsed;
  }

  // Setup hover functionality
  function setupHoverBehavior(sidebar) {
    let hoverTimeout;

    sidebar.addEventListener('mouseenter', () => {
      if (!sidebar.classList.contains('dragging')) {
        clearTimeout(hoverTimeout);
        sidebar.classList.add('hovering');
      }
    });

    sidebar.addEventListener('mouseleave', () => {
      if (!sidebar.classList.contains('dragging')) {
        clearTimeout(hoverTimeout);
        hoverTimeout = setTimeout(() => {
          sidebar.classList.remove('hovering');
        }, 234);
      }
    });
  }

  // Main initialization
  function init(isFirstLoad = false) {
    const existing = document.querySelector(`[${DOM_MARK}]`);
    if (existing) {
      existing._cleanup?.();
      existing._cleanupDrag?.();
      existing.remove();
    }

    const questionEls = queryQuestionEls();
    if (questionEls.length === 0) return;

    const sidebar = createSidebar();
    document.body.appendChild(sidebar);

    sidebar._isManuallyToggled = false;
    sidebar._isOpen = false;
    sidebar.classList.add('collapsed');

    const list = sidebar.querySelector('.questions-list');

    // Trạng thái "dính cuối"
    sidebar._stickToBottom = true; // lần đầu luôn coi như dính cuối
    sidebar._forceBottomUntil = Date.now() + FORCE_STICK_BOTTOM_MS;

    // Theo dõi scroll của sidebar để biết khi nào người dùng kéo lên
    list.addEventListener('scroll', () => {
      const atBottom = (list.scrollTop + list.clientHeight >= list.scrollHeight - 5);
      sidebar._stickToBottom = atBottom;
    });

    // Click câu hỏi
    list.addEventListener('click', (e) => handleQuestionClick(e, sidebar));

    setupHoverBehavior(sidebar);
    setupDragBehavior(sidebar);

    // Lần đầu: build list và chỉ cuộn sidebar về cuối (không đụng tới chat)
    updateQuestionsList(sidebar, isFirstLoad);

    const scrollContainer = queryChatContainer()?.parentElement;
    if (scrollContainer) {
      let scrollTimeout;
      scrollContainer.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
          updateActiveQuestion(sidebar, queryQuestionEls());
        }, 50);
      });
    }

    // Quan sát thay đổi để cập nhật danh sách
    const observer = new MutationObserver(() => {
      updateQuestionsList(sidebar);
    });

    const chatContainer = queryChatContainer();
    if (chatContainer) {
      observer.observe(chatContainer, { childList: true });
    }

    sidebar._cleanup = () => {
      observer.disconnect();
    };
  }

  // Monitor for changes
  let loaded = false;
  let conversationId = null;
  let isInitialLoad = true;

  setInterval(() => {
    const latestConversationId = getConversationIdByUrl();
    const hasQuestions = queryQuestionEls().length > 0;

    if (conversationId !== latestConversationId || !hasQuestions) {
      conversationId = latestConversationId;
      const existing = document.querySelector(`[${DOM_MARK}]`);
      if (existing) {
        existing._cleanup?.();
        existing._cleanupDrag?.();
        existing.remove();
      }
      loaded = false;

      if (conversationId !== latestConversationId) {
        isInitialLoad = true;
      }
    }

    if (!loaded && hasQuestions) {
      init(isInitialLoad);
      loaded = true;
      isInitialLoad = false;
    }
  }, 600);
})();

Lưu lại và sau đó tận hưởng

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Back to top button