// @version 2023-11-11 14:34:28
// @author ttmouse & GPT-4
// @match https://chat.openai.com/*
////////////////////////// CSS //////////////////////////
background-color: #7575751a;
white-space: nowrap; /* 不折行 */
overflow: hidden; /* 隐藏超出的内容 */
text-overflow: ellipsis; /* 用...来表示溢出的文本 */
white-space: nowrap; /* 不折行 */
overflow: hidden; /* 隐藏超出的内容 */
text-overflow: ellipsis; /* 用...来表示溢出的文本 */
background-color: #0000005c;
white-space: nowrap; /* 不折行 */
overflow: hidden; /* 隐藏超出的内容 */
text-overflow: ellipsis; /* 用...来表示溢出的文本 */
height: 70px; /* 你想要保留的顶部距离 */
background-color: #c4c4c4;
background: rgb(204 204 204 / 39%);
background: rgb(204 204 204 / 0%);
const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);
////////////////////////// JS //////////////////////////
let isMouseDown = false;
let index = 1; // 用于生成目录项的序号
function createDirectoryContainer() {
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory');
const resizeHandle = document.createElement('div');
resizeHandle.id = 'resizeHandle';
resizeHandle.classList.add('resizeHandle');
directoryDiv.appendChild(resizeHandle);
function setChatTitle(directoryTitle) {
const activeMenuElement = document.querySelector('a.bg-token-surface-primary:not(:hover)');
let chatTitle = activeMenuElement ? activeMenuElement.textContent.trim() : 'Title';
directoryTitle.innerText = chatTitle;
directoryTitle.id = 'directoryTitle';
directoryTitle.classList.add('directoryTitle');
function createDirectoryTitle(directoryDiv) {
const directoryTitle = document.createElement('div');
setChatTitle(directoryTitle);
directoryTitle.addEventListener('mousedown', function(event) {
startDrag(event, directoryDiv);
function startDrag(event, directoryDiv) {
mouseDownTimer = setTimeout(() => {
initiateDrag(event, directoryDiv);
function initiateDrag(event, directoryDiv) {
const offsetX = event.clientX - directoryDiv.offsetLeft;
const offsetY = event.clientY - directoryDiv.offsetTop;
document.addEventListener('mousemove', (event) => {
directoryDiv.style.left = `${event.clientX - offsetX}px`;
directoryDiv.style.top = `${event.clientY - offsetY}px`;
function addDirectoryEntries(directoryDiv, resizeHandle) {
const userMessages = document.querySelectorAll('div[data-message-author-role="user"]>div'); // 获取所有用户消息
userMessages.forEach((msg, i) => { // 遍历所有用户消息
const directoryEntry = createDirectoryEntry(msg, i);
directoryDiv.appendChild(directoryEntry);
index = userMessages.length + 1;
directoryDiv.appendChild(resizeHandle); // 将resizeHandle放在最后
function createDirectoryEntry(msg, i) {
// const text = msg ? msg.innerText.split('\n')[0] : '';
const text = msg ? msg.innerText.split('\n').find(line => line.trim() !== '') : '';
const directoryEntry = document.createElement('div');
directoryEntry.className = 'truncate';
directoryEntry.innerText = `${i + 1}. ${text}`;
directoryEntry.setAttribute('data-index', i);
directoryEntry.addEventListener('click', () => {
directoryEntry.addEventListener('dblclick', () => {
// 009 滚动到对应消息位置,距离顶部70px
function scrollToMessage(msg) {
performScrollAdjustment();
const grandGrandParent = msg.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
const scrollContainer = grandGrandParent.closest('div[class*="react-scroll-to-bottom--css-"]');
const elementPosition = grandGrandParent.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;
const offset = 70; // 自定义间距
const targetScrollTop = scrollContainer.scrollTop + elementPosition - offset;
smoothScroll(scrollContainer, targetScrollTop, 500); // 500ms 的滚动动画
function smoothScroll(element, target, duration) {
const start = element.scrollTop;
const change = target - start;
function animateScroll() {
currentTime += increment;
const val = Math.easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
Math.easeInOutQuad = function (t, b, c, d) {
if (t < 1) return c / 2 * t * t + b;
return -c / 2 * (t * (t - 2) - 1) + b;
function performScrollAdjustment() {
const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
let parentContainer = parentContainers.length ? parentContainers[0] : null;
let actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
if (actualScrollContainer) {
const originalScrollTop = actualScrollContainer.scrollTop;
actualScrollContainer.scrollTop = originalScrollTop - 1;
function createPlaceholder() {
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
function editMessage(msg) {
const grandGrandParent = msg.parentElement?.parentElement?.parentElement;
const secondChild = grandGrandParent.children[1];
const editButton = secondChild.querySelector('button');
setTimeout(() => focusOnTextarea(grandGrandParent), 10);
function focusOnTextarea(grandGrandParent) {
// 由于 DOM 结构已变更,直接在上上上级元素中查找 textarea
const textareaElement = grandGrandParent.querySelector('textarea');
textareaElement.focus();
// 设置文本选择范围为整个文本,以实现全选效果
textareaElement.setSelectionRange(0, textareaElement.value.length);
function setupResizeHandle(resizeHandle, directoryDiv) {
resizeHandle.addEventListener('mousedown', (event) => {
document.addEventListener('mousemove', (event) => handleMouseMove(event, directoryDiv));
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', (event) => handleMouseMove(event, directoryDiv));
function handleMouseMove(event, directoryDiv) {
const dx = event.clientX - lastX;
directoryDiv.style.width = `${parseInt(getComputedStyle(directoryDiv).width, 10) + dx}px`;
function initDirectory() {
if (document.querySelector('#directory')) {
return; // 如果目录已存在,不再重复创建
const directoryDiv = createDirectoryContainer();
const directoryTitle = createDirectoryTitle(directoryDiv);
directoryDiv.appendChild(directoryTitle);
const resizeHandle = directoryDiv.querySelector('#resizeHandle');
addDirectoryEntries(directoryDiv, resizeHandle);
setupResizeHandle(resizeHandle, directoryDiv);
const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
parentContainer.appendChild(directoryDiv);
document.body.appendChild(directoryDiv);
document.addEventListener('mouseup', () => {
clearTimeout(mouseDownTimer);
function observePerformanceChanges(callback) { // 监控页面是否更新
const observer = new PerformanceObserver((list) => { // 监控页面性能
for (const entry of list.getEntries()) { // 遍历所有性能条目
if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) { // 如果条目名称包含指定字符串
observer.observe({ entryTypes: ['resource'] });
// 019 重构后的重新生成目录函数,识别到分享按钮后再生成目录
function regenerateDirectoryOnContentUpdate() {
let intervalId = setInterval(() => {
const shareButton = document.querySelector('button svg[viewBox="0 0 24 24"][fill="none"]');
regenerateDirectory(); // 识别到分享按钮后,重新生成目录
clearInterval(intervalId); // 识别到分享按钮后,停止循环
function regenerateDirectory() {
const directoryDiv = document.querySelector('#directory');
directoryDiv.innerHTML = ''; // 清空目录内容
const directoryTitle = createDirectoryTitle(directoryDiv);
directoryDiv.appendChild(directoryTitle);
const resizeHandle = document.createElement('div');
resizeHandle.id = 'resizeHandle';
resizeHandle.classList.add('resizeHandle');
addDirectoryEntries(directoryDiv, resizeHandle);
setupResizeHandle(resizeHandle, directoryDiv);
// 重新绑定滚动事件监听器以确保高亮功能正常工作
function observeArrowButtons() {
const allButtonElements = document.querySelectorAll('button');
allButtonElements.forEach(buttonElement => {
const svgElement = buttonElement.querySelector('svg');
const polylineElement = svgElement.querySelector('polyline');
const pointsAttr = polylineElement ? polylineElement.getAttribute('points') : '';
if (pointsAttr === "15 18 9 12 15 6" || pointsAttr === "9 18 15 12 9 6") {
buttonElement.addEventListener('click', () => {
setTimeout(regenerateDirectory, 300);
function highlightDirectoryEntry() {
const userMessageTextDivs = document.querySelectorAll('div[data-message-author-role="user"]>div');
let closestToTop = null;
userMessageTextDivs.forEach((textDiv, i) => {
const userMessageDiv = textDiv.parentElement;
const rect = userMessageDiv.getBoundingClientRect();
const directoryEntry = document.querySelector(`[data-index="${i}"]`);
directoryEntry.classList.remove('highlight');
if (rect.top >= 0 && rect.top < minTop) {
closestToTop = directoryEntry;
closestToTop.classList.add('highlight');
function setupScrollListener() {
const scrollContainer = document.querySelector('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
scrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
observePerformanceChanges(regenerateDirectoryOnContentUpdate);