// ==UserScript==
// @name 提问问题目录列表导航生成器(ChatGPT 和 Perplexity.ai)- 优化版本
// @name:zh-CN 提问问题目录列表导航生成器(ChatGPT 和 Perplexity.ai)- 优化版本
// @name:zh-TW 提問目錄生成器(ChatGPT & Perplexity.ai)
// @name:en Question Directory Generator (ChatGPT & Perplexity.ai)
// @version 1.0.3
// @author 9dian9
// @namespace https://github.com/KelvienLee/question-directory-script
// @description 为 ChatGPT 和 Perplexity.ai 提供提问目录导航功能,支持点击跳转至问题开头。
// @description:zh-CN 提供 ChatGPT 和 Perplexity.ai 提问目录功能,点击目录可跳转到问题开头。
// @description:zh-TW 提供 ChatGPT 和 Perplexity.ai 提問目錄功能,點選目錄可跳轉至問題開頭。
// @description:en Adds a question directory for ChatGPT and Perplexity.ai, enabling smooth scroll to question start.
// @match https://chatgpt.com/*
// @match https://www.perplexity.ai/*
// @grant none
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// 这里可自由调整,设置滚动到问题时保留多少顶部空间
// 如果有固定导航栏,可适度增大这个值
const SCROLL_MARGIN = 100;
let observer;
let lastUrl = window.location.href;
/**
* 初始化
*/
function init() {
const siteType = detectSite();
if (!siteType) {
console.warn('不支持的网站');
return;
}
const chatContainer = siteType === 'chatgpt'
? document.querySelector('div.m-auto.text-base')
: document.querySelector('main'); // Perplexity 的主内容区
if (!chatContainer) {
console.warn('未找到聊天容器,稍后重试');
return;
}
if (document.getElementById('question-directory-container')) return;
// 创建提问目录容器
const container = document.createElement('div');
container.id = 'question-directory-container';
container.style.cssText = `
position: fixed;
top: 150px;
right: 10px;
z-index: 9999;
font-family: "Arial", sans-serif;
`;
document.body.appendChild(container);
// 创建展开/收起按钮
const toggleButton = document.createElement('button');
toggleButton.id = 'toggle-question-directory';
toggleButton.textContent = ' ▼';
toggleButton.style.cssText = `
position: absolute;
right: 0;
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
`;
toggleButton.addEventListener('mouseover', () => {
toggleButton.style.backgroundColor = '#45A049';
toggleButton.style.boxShadow = '0px 6px 12px rgba(0, 0, 0, 0.2)';
});
toggleButton.addEventListener('mouseout', () => {
toggleButton.style.backgroundColor = '#4CAF50';
toggleButton.style.boxShadow = '0px 4px 8px rgba(0, 0, 0, 0.1)';
});
container.appendChild(toggleButton);
// 创建目录面板
const directory = document.createElement('div');
directory.id = 'question-directory';
directory.style.cssText = `
display: none; /* 默认收起 */
margin-top: 10px;
width: 300px;
max-height: 70vh;
overflow-y: auto;
background-color: #fff;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
`;
directory.innerHTML = '
';
container.appendChild(directory);
// 展开/收起事件
toggleButton.addEventListener('click', () => {
const isHidden = directory.style.display === 'none';
directory.style.display = isHidden ? 'block' : 'none';
toggleButton.textContent = ` ${isHidden ? '▲' : '▼'}`;
});
// 监听 DOM 变动,自动更新问题列表
observer = new MutationObserver(updateQuestions);
observer.observe(chatContainer, { childList: true, subtree: true });
// 初始化先更新一次
updateQuestions();
// 监听 URL 变化,一旦切换话题/链接,也重置
const urlObserver = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
lastUrl = window.location.href;
setTimeout(updateQuestions, 1000);
}
});
urlObserver.observe(document.body, { childList: true, subtree: true });
}
/**
* 判断当前站点
*/
function detectSite() {
if (window.location.hostname.includes('chatgpt.com')) return 'chatgpt';
if (window.location.hostname.includes('perplexity.ai')) return 'perplexity';
return null;
}
/**
* 截断文本
*/
function truncateText(text, length = 20) {
if (text.length > length) {
return text.substring(0, length) + '...';
}
return text;
}
/**
* 更新问题列表
*/
function updateQuestions() {
const siteType = detectSite();
const questionSelector = siteType === 'chatgpt'
? 'div[data-message-author-role="user"] .whitespace-pre-wrap'
: '[class*="group/query"].relative.whitespace-pre-line.break-words';
const userMessages = document.querySelectorAll(questionSelector);
const listElement = document.querySelector('#questions-list');
if (!userMessages.length || !listElement) return;
listElement.innerHTML = '';
// 为了让 scrollIntoView 更精确,给每个元素设置 scrollMarginTop
// 让他们在滚动的时候,会多空出一段顶部距离
userMessages.forEach(msg => {
msg.style.scrollMarginTop = `${SCROLL_MARGIN}px`;
});
Array.from(userMessages).forEach((msg, i) => {
const questionText = msg.innerText.trim();
if (questionText) {
const truncatedText = truncateText(questionText, 40);
const questionId = `question-${i + 1}`;
msg.id = questionId; // 标记下,备用
const listItem = document.createElement('li');
listItem.style.cssText = `
margin-bottom: 8px;
padding: 6px 10px;
border-radius: 6px;
transition: background-color 0.3s ease;
cursor: pointer;
`;
listItem.innerHTML = `${i + 1}. ${truncatedText}`;
// 跳转到问题元素
listItem.addEventListener('click', (event) => {
event.preventDefault();
msg.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
});
// 简单 hover 效果
listItem.addEventListener('mouseover', () => {
listItem.style.backgroundColor = '#f0f0f0';
});
listItem.addEventListener('mouseout', () => {
listItem.style.backgroundColor = 'transparent';
});
listElement.appendChild(listItem);
}
});
}
// DOMContentLoaded 后再初始化
window.addEventListener('DOMContentLoaded', () => {
console.log('DOM 已加载,初始化提问目录');
setTimeout(init, 200);
});
// 在 document 级别点击时刷新下列表,以防出现漏掉
document.addEventListener('click', updateQuestions);
// 如果目录莫名消失,就重新 init
setInterval(() => {
if (!document.getElementById('question-directory-container')) {
console.warn('提问目录丢失,重新初始化');
init();
}
}, 3000);
})();