跳转到内容

豆爸:GPT 生成历史记录目录

目录的功能

  1. 自动生成目录.
  2. 目录支持点击跳转,需要在页面上先用鼠标滚动一下,再点击。在页面最底部的时候无法点击目录。
  3. 双击目录项可以打开编辑模式。对历史内容进行编辑.
  4. 目录支持拖动改变目录位置,点击目录头部拖动.
  5. 支持目录高亮展示当前位置. 2023-10-15 00:02:13
  6. 自动适配深色浅色皮肤 2023-10-25 01:16:12

需要通过油猴安装, 参考南瓜博士:用GPT写的油猴脚本

更新日志

安装最顶部的版本就好了。

  1. 修了个在最底部点击目录跳转会出现问题的bug 2023-10-15 00:45:16
  2. 添加了跳转目录的缓动效果. 有动画 2023-10-15 00:50:56
  3. 修复会把"Today"识别出来的bug, 2023-10-21 16:35:24
  4. GPT的css调整导致失效,重新定位对象 2023-10-25 00:50:21
  5. 增加了皮肤颜色的适配,原来仅支持浅色皮肤,现在支持深色了。
  6. 官网更新,导致高亮无效,已经修复, 2023-10-30 11:04:44
  7. 支持目录调整宽度
  8. 支持新版gpt 2023-11-12 11:27:03

支持GPT新版,支持双击进入编辑。

还差一个取消编辑按钮无法恢复监听,暂不管了。

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      2023-11-11 14:34:28
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

////////////////////////// CSS //////////////////////////
const style = `

/* 浅色皮肤 */
.light #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

.truncate {
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
  }

/* 深色皮肤 */
.dark #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #0000005c;
    color: #e0e0e0c7;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    line-height: 170%;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}

.resizeHandle:hover {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 39%);
    position: absolute;
    right: 0px;
    top: 0px;
}
.resizeHandle {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 0%);
    position: absolute;
    right: 0px;
    top: 0px;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////

// 001 Global State
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;
let isResizing = false;
let lastX;
let index = 1; // 用于生成目录项的序号

// 002 创建目录容器
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);

    return directoryDiv;
}

// 003 设置目录标题
function setChatTitle(directoryTitle) {
    const activeMenuElement = document.querySelector('a.bg-token-surface-primary:not(:hover)');
    let chatTitle = activeMenuElement ? activeMenuElement.textContent.trim() : 'Title';

    if (directoryTitle) {
        directoryTitle.innerText = chatTitle;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle');
    }
}

// 004 创建目录标题
function createDirectoryTitle(directoryDiv) {
    const directoryTitle = document.createElement('div');
    setChatTitle(directoryTitle);

    directoryTitle.addEventListener('mousedown', function(event) {
        startDrag(event, directoryDiv);
    });

    return directoryTitle;
}

// 005 处理鼠标按下事件
function startDrag(event, directoryDiv) {
    isMouseDown = true;
    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;
            initiateDrag(event, directoryDiv);
        }
    }, 150);
}

// 006 处理鼠标移动事件
function initiateDrag(event, directoryDiv) {
    const offsetX = event.clientX - directoryDiv.offsetLeft;
    const offsetY = event.clientY - directoryDiv.offsetTop;

    document.addEventListener('mousemove', (event) => {
        if (isDragging) {
            directoryDiv.style.left = `${event.clientX - offsetX}px`;
            directoryDiv.style.top = `${event.clientY - offsetY}px`;
        }
    });
}

// 007 获取所有用户消息
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放在最后
}

// 008 创建目录项
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', () => {
        scrollToMessage(msg);
    });

    directoryEntry.addEventListener('dblclick', () => {
        editMessage(msg);
    });

    return directoryEntry;
}

// 009 滚动到对应消息位置,距离顶部70px
function scrollToMessage(msg) {
    performScrollAdjustment();

    setTimeout(() => {
        const grandGrandParent = msg.parentElement?.parentElement?.parentElement?.parentElement?.parentElement; 
        // 获取消息的爷爷爷爷爷爷爷爷爷节点
        if (grandGrandParent) {
            const scrollContainer = grandGrandParent.closest('div[class*="react-scroll-to-bottom--css-"]');

            if (scrollContainer) {
                const elementPosition = grandGrandParent.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;
                const offset = 70; // 自定义间距
                const targetScrollTop = scrollContainer.scrollTop + elementPosition - offset;

                smoothScroll(scrollContainer, targetScrollTop, 500); // 500ms 的滚动动画
            }
        }
    }, 10);
}

// 009.1 平滑滚动函数
function smoothScroll(element, target, duration) {
    const start = element.scrollTop;
    const change = target - start;
    let currentTime = 0;
    const increment = 20;

    function animateScroll() {
        currentTime += increment;
        const val = Math.easeInOutQuad(currentTime, start, change, duration);
        element.scrollTop = val;
        if (currentTime < duration) {
            requestAnimationFrame(animateScroll);
        }
    }

    animateScroll();
}

// 009.2 缓动函数
Math.easeInOutQuad = function (t, b, c, d) {
    t /= d / 2;
    if (t < 1) return c / 2 * t * t + b;
    t--;
    return -c / 2 * (t * (t - 2) - 1) + b;
};

// 010 执行小幅度滚动调整
function performScrollAdjustment() {
    const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
    let parentContainer = parentContainers.length ? parentContainers[0] : null;

    if (parentContainer) {
        let actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
        if (actualScrollContainer) {
            const originalScrollTop = actualScrollContainer.scrollTop;
            actualScrollContainer.scrollTop = originalScrollTop - 1;
        }
    }
}

// 011 创建占位符
function createPlaceholder() {
    const placeholder = document.createElement('div');
    placeholder.className = 'placeholder';
    return placeholder;
}

// 012 模拟编辑消息的按钮点击
function editMessage(msg) {
    // 获取消息的上上上级元素
    const grandGrandParent = msg.parentElement?.parentElement?.parentElement;

    if (grandGrandParent) {
        // 定位到上上上级元素的第二个子元素
        const secondChild = grandGrandParent.children[1];
        if (secondChild) {
            // 在第二个子元素的子元素中查找编辑按钮
            const editButton = secondChild.querySelector('button');
            if (editButton) {
                editButton.click();
                focusOnTextarea(msg);
                setTimeout(() => focusOnTextarea(grandGrandParent), 10);
            }
        }
    }
}

// 013 聚焦到文本框并全选文本
function focusOnTextarea(grandGrandParent) {
    // 由于 DOM 结构已变更,直接在上上上级元素中查找 textarea
    const textareaElement = grandGrandParent.querySelector('textarea');
    if (textareaElement) {
        textareaElement.focus();
        // 设置文本选择范围为整个文本,以实现全选效果
        textareaElement.setSelectionRange(0, textareaElement.value.length);
    }
    
}

// 014 处理调整大小的逻辑
function setupResizeHandle(resizeHandle, directoryDiv) {
    resizeHandle.addEventListener('mousedown', (event) => {
        isResizing = true;
        lastX = event.clientX;
        document.addEventListener('mousemove', (event) => handleMouseMove(event, directoryDiv));
        document.addEventListener('mouseup', () => {
            isResizing = false;
            document.removeEventListener('mousemove', (event) => handleMouseMove(event, directoryDiv));
        });
    });
}

// 015 处理鼠标移动事件
function handleMouseMove(event, directoryDiv) {
    if (isResizing) {
        const dx = event.clientX - lastX;
        lastX = event.clientX;
        directoryDiv.style.width = `${parseInt(getComputedStyle(directoryDiv).width, 10) + dx}px`;
    }
}

// 016 初始化目录
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');
    if (parentContainer) {
        parentContainer.appendChild(directoryDiv);
    } else {
        document.body.appendChild(directoryDiv);
    }
}

// 017 主入口
initDirectory();
document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

// 018 监控页面是否更新
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')) { // 如果条目名称包含指定字符串
                console.log("检测到内容更新");
                callback(); // 执行回调函数
            }
        }
    });
    observer.observe({ entryTypes: ['resource'] });
}

// 019 重构后的重新生成目录函数,识别到分享按钮后再生成目录
function regenerateDirectoryOnContentUpdate() { 
    let intervalId = setInterval(() => {
        const shareButton = document.querySelector('button svg[viewBox="0 0 24 24"][fill="none"]');
        if (shareButton) {
            regenerateDirectory(); // 识别到分享按钮后,重新生成目录
            clearInterval(intervalId); // 识别到分享按钮后,停止循环
        }
    }, 50);
}

// 020 重构后的重新生成目录函数
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);
    // 重新绑定滚动事件监听器以确保高亮功能正常工作
    setupScrollListener();
}

// 021 监听左右箭头按钮点击事件
function observeArrowButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                });
            }
        }
    });
}

// 022 目录高亮展示
function highlightDirectoryEntry() {
    const userMessageTextDivs = document.querySelectorAll('div[data-message-author-role="user"]>div');
    let closestToTop = null;
    let minTop = Infinity;

    userMessageTextDivs.forEach((textDiv, i) => {
        const userMessageDiv = textDiv.parentElement;
        if (userMessageDiv) {
            const rect = userMessageDiv.getBoundingClientRect();
            const directoryEntry = document.querySelector(`[data-index="${i}"]`);
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');

                if (rect.top >= 0 && rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// 023 监听滚动事件
function setupScrollListener() {
    const scrollContainer = document.querySelector('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
    if (scrollContainer) {
        scrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

// 024 初始化函数
function initialize() {
    initDirectory();
    observePerformanceChanges(regenerateDirectoryOnContentUpdate);
    observeArrowButtons();
    setupScrollListener();
}

initialize();

仅保留用户输入的内容

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.3
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

/* 浅色皮肤 */
.light #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

.truncate {
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
  }

/* 深色皮肤 */
.dark #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #0000005c;
    color: #e0e0e0c7;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    line-height: 170%;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}

.resizeHandle:hover {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 39%);
    position: absolute;
    right: 0px;
    top: 0px;
}
.resizeHandle {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 0%);
    position: absolute;
    right: 0px;
    top: 0px;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类
const directoryItems = []; 

// 添加 resizeHandle 到 directoryDiv
const resizeHandle = document.createElement('div');
resizeHandle.id = 'resizeHandle';
resizeHandle.classList.add('resizeHandle'); // 添加CSS类
directoryDiv.appendChild(resizeHandle);

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

function appendDirectoryToParent() {
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');   

    parentContainer.appendChild(directoryDiv);
}

// B01. 生成目录
function initDirectory() {

    appendDirectoryToParent()
    console.log("B00. 将目录容器添加到页面");
    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.gizmo\\:w-full:not(.agent-turn)');

    userMessages.forEach((msg, i) => {
        // const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';  // 获取用户消息的前10个字
        const text = msg ? msg.innerText.split('\n')[0] : '';  
        const directoryEntry = document.createElement('div');
        directoryEntry.className = 'truncate';  // 添加CSS类
        directoryEntry.innerText = `${i + 1}. ${text}`;

        directoryEntry.setAttribute('data-index', i);  // 为每个目录项添加一个自定义属性,用于标识对应的用户消息的索引

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            // 模拟滚动来解决在底部无法点击目录的问题
            // 找到父容器
            const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
            let parentContainer = null;

            for (const container of parentContainers) {
                if (container.classList.contains('h-full')) {  // 用其他不会变的类名进一步确认
                    parentContainer = container;
                    break;
                }
            }
            // 从父容器找到实际的滚动容器
            let actualScrollContainer = null;
            if (parentContainer) {
                actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
            }

            // 进行模拟滚动
            if (actualScrollContainer) {
                const originalScrollTop = actualScrollContainer.scrollTop;
                actualScrollContainer.scrollTop = originalScrollTop - 1;
            }

            // 延迟一小段时间后进行跳转
            setTimeout(() => {
                const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
                if (grandGrandParent) {
                    const parentOfGrandGrandParent = grandGrandParent.parentElement;
                    if (parentOfGrandGrandParent) {
                        parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                    }

                    // 跳转到对应位置,有缓动效果
                    placeholder.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});

                    setTimeout(() => {
                        placeholder.remove();
                    }, 0);  // 延迟500毫秒以确保滚动操作完成
                }
            }, 0); // 延迟200毫秒

        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        directoryDiv.appendChild(directoryEntry);
        directoryDiv.appendChild(resizeHandle);

    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    
    appendDirectoryToParent()

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
    ScrollEventListener() 
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

// F01. 基于当前页面展示的内容,来让菜单对应目录部分高亮展示。 以下是代码:
function highlightDirectoryEntry() {
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.gizmo\\:w-full:not(.agent-turn)');
    let closestToTop = null;
    let minTop = Infinity;

    userMessages.forEach((msg, i) => { // 遍历所有用户消息
        const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;  // 找到用户消息的祖先元素
        if (grandGrandParent) { // 确保找到了祖先元素
            const rect = grandGrandParent.getBoundingClientRect(); // 获取元素的位置信息
            // console.log(`Element at index ${i} position:`, rect);  // 打印元素位置信息
            const directoryEntry = document.querySelector(`[data-index="${i}"]`); // 获取对应的目录项

            // 移除所有高亮
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');
            }

            // 记录最接近顶部但仍在视窗内的元素
            if (rect.top >= 0 && rect.top <= window.innerHeight) {  
                if (rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    // 只高亮最接近顶部的元素
    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// F02. 监听滚动事件
function ScrollEventListener() {
// 找到实际的滚动容器
    const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
    let parentContainer = null;

    for (const container of parentContainers) {
        if (container.classList.contains('h-full')) {  // 用其他不会变的类名进一步确认
            parentContainer = container;
            break;
        }
    }

    // 再找到子对象(滚动容器)
    let actualScrollContainer = null;
    if (parentContainer) {
        actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
    }

    // 绑定滚动事件监听器到实际的滚动容器
    if (actualScrollContainer) {
        console.log("F02 找到了实际的滚动容器");
        actualScrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

let isResizing = false;
let lastX;

// 监听mousedown事件
resizeHandle.addEventListener('mousedown', (event) => {
    isResizing = true;
    lastX = event.clientX;
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', () => {
        // 停止调整大小
        isResizing = false;
        document.removeEventListener('mousemove', handleMouseMove);
    });
});

// 处理mousemove事件
function handleMouseMove(event) {
    if (isResizing) {
        const dx = event.clientX - lastX;
        lastX = event.clientX;

        // 更新目录的宽度
        directoryDiv.style.width = (parseInt(getComputedStyle(directoryDiv).width, 10) + dx) + 'px';
    }
}

// H01. 在页面加载完成后运行初始化函数

// 初始化后
initDirectory();
// highlightDirectoryEntry();

observeButtons();
console.log("E01. 加载完成后的初始化");
regenerateDirectory();
ScrollEventListener()

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮
- 知识目录高亮 
- 修了个bug,底部也可以用了 2023-10-15 00:47:48

*/

GPT升级导致高亮目录无效,已经修复,支持了目录调整宽度 2023-10-30 11:05:05

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.3
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

/* 浅色皮肤 */
.light #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

.truncate {
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
  }

/* 深色皮肤 */
.dark #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #0000005c;
    color: #e0e0e0c7;
    border-radius: 5px;
    padding: 5px;
    width: 190px;
    font-size: 15px;
    line-height: 170%;
    display: inline-block;
    white-space: nowrap;  /* 不折行 */
    overflow: hidden;      /* 隐藏超出的内容 */
    text-overflow: ellipsis;  /* 用...来表示溢出的文本 */
}

#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}

.resizeHandle:hover {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 39%);
    position: absolute;
    right: 0px;
    top: 0px;
}
.resizeHandle {
    width: 3px;
    height: 100%;
    cursor: ew-resize;
    background: rgb(204 204 204 / 0%);
    position: absolute;
    right: 0px;
    top: 0px;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类
const directoryItems = []; 

// 添加 resizeHandle 到 directoryDiv
const resizeHandle = document.createElement('div');
resizeHandle.id = 'resizeHandle';
resizeHandle.classList.add('resizeHandle'); // 添加CSS类
directoryDiv.appendChild(resizeHandle);

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

function appendDirectoryToParent() {
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    parentContainer.appendChild(directoryDiv);
}

// B01. 生成目录
function initDirectory() {

    appendDirectoryToParent()
    console.log("B00. 将目录容器添加到页面");
    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.gizmo\\:w-full');

    userMessages.forEach((msg, i) => {
        // const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';  // 获取用户消息的前10个字
        const text = msg ? msg.innerText.split('\n')[0] : '';  
        const directoryEntry = document.createElement('div');
        directoryEntry.className = 'truncate';  // 添加CSS类
        directoryEntry.innerText = `${i + 1}. ${text}`;

        directoryEntry.setAttribute('data-index', i);  // 为每个目录项添加一个自定义属性,用于标识对应的用户消息的索引

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            // 模拟滚动来解决在底部无法点击目录的问题
            // 找到父容器
            const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
            let parentContainer = null;

            for (const container of parentContainers) {
                if (container.classList.contains('h-full')) {  // 用其他不会变的类名进一步确认
                    parentContainer = container;
                    break;
                }
            }
            // 从父容器找到实际的滚动容器
            let actualScrollContainer = null;
            if (parentContainer) {
                actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
            }

            // 进行模拟滚动
            if (actualScrollContainer) {
                const originalScrollTop = actualScrollContainer.scrollTop;
                actualScrollContainer.scrollTop = originalScrollTop - 1;
            }

            // 延迟一小段时间后进行跳转
            setTimeout(() => {
                const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
                if (grandGrandParent) {
                    const parentOfGrandGrandParent = grandGrandParent.parentElement;
                    if (parentOfGrandGrandParent) {
                        parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                    }

                    // 跳转到对应位置,有缓动效果
                    placeholder.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});

                    setTimeout(() => {
                        placeholder.remove();
                    }, 0);  // 延迟500毫秒以确保滚动操作完成
                }
            }, 0); // 延迟200毫秒

        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        directoryDiv.appendChild(directoryEntry);
        directoryDiv.appendChild(resizeHandle);

    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    
    appendDirectoryToParent()

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
    ScrollEventListener() 
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

// F01. 基于当前页面展示的内容,来让菜单对应目录部分高亮展示。 以下是代码:
function highlightDirectoryEntry() {
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.gizmo\\:w-full'); 
    let closestToTop = null;
    let minTop = Infinity;

    userMessages.forEach((msg, i) => { // 遍历所有用户消息
        const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;  // 找到用户消息的祖先元素
        if (grandGrandParent) { // 确保找到了祖先元素
            const rect = grandGrandParent.getBoundingClientRect(); // 获取元素的位置信息
            // console.log(`Element at index ${i} position:`, rect);  // 打印元素位置信息
            const directoryEntry = document.querySelector(`[data-index="${i}"]`); // 获取对应的目录项

            // 移除所有高亮
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');
            }

            // 记录最接近顶部但仍在视窗内的元素
            if (rect.top >= 0 && rect.top <= window.innerHeight) {  
                if (rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    // 只高亮最接近顶部的元素
    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// F02. 监听滚动事件
function ScrollEventListener() {
// 找到实际的滚动容器
    const parentContainers = document.querySelectorAll('div[class*="react-scroll-to-bottom--css-"][class*="h-full"]');
    let parentContainer = null;

    for (const container of parentContainers) {
        if (container.classList.contains('h-full')) {  // 用其他不会变的类名进一步确认
            parentContainer = container;
            break;
        }
    }

    // 再找到子对象(滚动容器)
    let actualScrollContainer = null;
    if (parentContainer) {
        actualScrollContainer = parentContainer.querySelector('div[class*="react-scroll-to-bottom--css-"]');
    }

    // 绑定滚动事件监听器到实际的滚动容器
    if (actualScrollContainer) {
        console.log("F02 找到了实际的滚动容器");
        actualScrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

let isResizing = false;
let lastX;

// 监听mousedown事件
resizeHandle.addEventListener('mousedown', (event) => {
    isResizing = true;
    lastX = event.clientX;
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', () => {
        // 停止调整大小
        isResizing = false;
        document.removeEventListener('mousemove', handleMouseMove);
    });
});

// 处理mousemove事件
function handleMouseMove(event) {
    if (isResizing) {
        const dx = event.clientX - lastX;
        lastX = event.clientX;

        // 更新目录的宽度
        directoryDiv.style.width = (parseInt(getComputedStyle(directoryDiv).width, 10) + dx) + 'px';
    }
}

// H01. 在页面加载完成后运行初始化函数

// 初始化后
initDirectory();
// highlightDirectoryEntry();

observeButtons();
console.log("E01. 加载完成后的初始化");
regenerateDirectory();
ScrollEventListener()

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮
- 知识目录高亮 
- 修了个bug,底部也可以用了 2023-10-15 00:47:48

*/

GPT升级之后导致失效,已经修复,且支持了深色皮肤的适配 2023-10-25 00:50:43

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.3
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

/* 浅色皮肤 */
.light #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: fit-content;
    font-size: 15px;
    display: inline-block;
}

/* 深色皮肤 */
.dark #directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #0000005c;
    color: #e0e0e0c7;
    border-radius: 5px;
    padding: 5px;
    width: fit-content;
    font-size: 15px;
    line-height: 170%;
    display: inline-block;
}

#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类
const directoryItems = []; 

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

// A04. 将新创建的元素添加到页面的body(或其他适当的位置)
// 找到父容器
// const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
// 将新创建的DIV添加到父容器之下
// parentContainer.appendChild(directoryDiv);
// 添加到页面
// console.log("A04. 将目录容器添加到页面");

function appendDirectoryToParent() {
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    parentContainer.appendChild(directoryDiv);
}

// B01. 生成目录
function initDirectory() {

    appendDirectoryToParent()
    console.log("B00. 将目录容器添加到页面");
    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.flex-col.gizmo\\:w-full');

    userMessages.forEach((msg, i) => {
        const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';
        const directoryEntry = document.createElement('div');
        directoryEntry.innerText = `${i + 1}. ${text}`;

        directoryEntry.setAttribute('data-index', i);  // 为每个目录项添加一个自定义属性,用于标识对应的用户消息的索引

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            // 找到父元素
            const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
            // 从父元素找到实际的滚动子元素(这里假设它是第一个子元素)
            const actualScrollContainerChild = actualScrollContainer ? actualScrollContainer.children[0] : null;
            // 模拟微小的滚动
            if (actualScrollContainerChild) {
                const originalScrollTop = actualScrollContainerChild.scrollTop;
                actualScrollContainerChild.scrollTop = originalScrollTop - 1;
            }

            // 延迟一小段时间后进行跳转
            setTimeout(() => {
                const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
                if (grandGrandParent) {
                    const parentOfGrandGrandParent = grandGrandParent.parentElement;
                    if (parentOfGrandGrandParent) {
                        parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                    }

                    // 跳转到对应位置,有缓动效果
                    placeholder.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});

                    setTimeout(() => {
                        placeholder.remove();
                    }, 0);  // 延迟500毫秒以确保滚动操作完成
                }
            }, 0); // 延迟200毫秒

        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        

        directoryDiv.appendChild(directoryEntry);
    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    
    appendDirectoryToParent()

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
    ScrollEventListener() 
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

// F01. 基于当前页面展示的内容,来让菜单对应目录部分高亮展示。 以下是代码:
function highlightDirectoryEntry() {
    const userMessages = document.querySelectorAll('.relative.flex.flex-col.flex-col.gizmo\\:w-full');
    let closestToTop = null;
    let minTop = Infinity;

    userMessages.forEach((msg, i) => {
        const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
        if (grandGrandParent) {
            const rect = grandGrandParent.getBoundingClientRect();
            const directoryEntry = document.querySelector(`[data-index="${i}"]`);

            // 移除所有高亮
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');
            }

            // 记录最接近顶部但仍在视窗内的元素
            if (rect.top >= 0 && rect.top <= window.innerHeight) {  // 修改这里,只检查顶部是否在视窗内
                if (rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    // 只高亮最接近顶部的元素
    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// F02. 监听滚动事件
function ScrollEventListener() {
// 找到实际的滚动容器
    const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
    // 绑定滚动事件监听器到实际的滚动容器
    if (actualScrollContainer) {
        console.log("F02 找到了实际的滚动容器");
        actualScrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

// G01. 在页面加载完成后运行初始化函数

// 初始化后
initDirectory();
// highlightDirectoryEntry();

observeButtons();
console.log("E01. 加载完成后的初始化");
regenerateDirectory();
ScrollEventListener()

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮
- 知识目录高亮 
- 修了个bug,底部也可以用了 2023-10-15 00:47:48

*/

修复会把"Today"识别出来的bug, 2023-10-21 16:35:24

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.3
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

#directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: fit-content;
    font-size: 15px;
    display: inline-block;
}
#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类
const directoryItems = []; 

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

// A04. 将新创建的元素添加到页面的body(或其他适当的位置)
// 找到父容器
// const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
// 将新创建的DIV添加到父容器之下
// parentContainer.appendChild(directoryDiv);
// 添加到页面
// console.log("A04. 将目录容器添加到页面");

function appendDirectoryToParent() {
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    parentContainer.appendChild(directoryDiv);
}

// B01. 生成目录
function initDirectory() {

    appendDirectoryToParent()
    console.log("B00. 将目录容器添加到页面");
    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.gizmo\\:text-gizmo-gray-600.gizmo\\:dark\\:text-gray-300');

    userMessages.forEach((msg, i) => {
        const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';
        const directoryEntry = document.createElement('div');
        directoryEntry.innerText = `${i + 1}. ${text}`;

        directoryEntry.setAttribute('data-index', i);  // 为每个目录项添加一个自定义属性,用于标识对应的用户消息的索引

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            // 找到父元素
            const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
            // 从父元素找到实际的滚动子元素(这里假设它是第一个子元素)
            const actualScrollContainerChild = actualScrollContainer ? actualScrollContainer.children[0] : null;
            // 模拟微小的滚动
            if (actualScrollContainerChild) {
                const originalScrollTop = actualScrollContainerChild.scrollTop;
                actualScrollContainerChild.scrollTop = originalScrollTop - 1;
            }

            // 延迟一小段时间后进行跳转
            setTimeout(() => {
                const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
                if (grandGrandParent) {
                    const parentOfGrandGrandParent = grandGrandParent.parentElement;
                    if (parentOfGrandGrandParent) {
                        parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                    }

                    // 跳转到对应位置,有缓动效果
                    placeholder.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});

                    setTimeout(() => {
                        placeholder.remove();
                    }, 0);  // 延迟500毫秒以确保滚动操作完成
                }
            }, 0); // 延迟200毫秒

        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        

        directoryDiv.appendChild(directoryEntry);
    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    
    appendDirectoryToParent()

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
    ScrollEventListener() 
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

// F01. 基于当前页面展示的内容,来让菜单对应目录部分高亮展示。 以下是代码:
function highlightDirectoryEntry() {
    const userMessages = document.querySelectorAll('.gizmo\\:text-gizmo-gray-600.gizmo\\:dark\\:text-gray-300');
    let closestToTop = null;
    let minTop = Infinity;

    userMessages.forEach((msg, i) => {
        const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
        if (grandGrandParent) {
            const rect = grandGrandParent.getBoundingClientRect();
            const directoryEntry = document.querySelector(`[data-index="${i}"]`);

            // 移除所有高亮
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');
            }

            // 记录最接近顶部但仍在视窗内的元素
            if (rect.top >= 0 && rect.top <= window.innerHeight) {  // 修改这里,只检查顶部是否在视窗内
                if (rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    // 只高亮最接近顶部的元素
    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// F02. 监听滚动事件
function ScrollEventListener() {
// 找到实际的滚动容器
    const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
    // 绑定滚动事件监听器到实际的滚动容器
    if (actualScrollContainer) {
        console.log("F02 找到了实际的滚动容器");
        actualScrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

// G01. 在页面加载完成后运行初始化函数

// 初始化后
initDirectory();
// highlightDirectoryEntry();

observeButtons();
console.log("E01. 加载完成后的初始化");
regenerateDirectory();
ScrollEventListener()

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮
- 知识目录高亮 
- 修了个bug,底部也可以用了 2023-10-15 00:47:48

*/

支持目录高亮 2023-10-15 00:45:46

  1. 支持目录高亮
// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.3
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

#directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: fit-content;
    font-size: 15px;
    display: inline-block;
}
#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

.highlight {
    background-color: #c4c4c4;
    color: #000;
    font-weight: 700;
}
`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类
const directoryItems = []; 

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

// A04. 将新创建的元素添加到页面的body(或其他适当的位置)
// 找到父容器
// const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
// 将新创建的DIV添加到父容器之下
// parentContainer.appendChild(directoryDiv);
// 添加到页面
// console.log("A04. 将目录容器添加到页面");

function appendDirectoryToParent() {
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    parentContainer.appendChild(directoryDiv);
}

// B01. 生成目录
function initDirectory() {

    appendDirectoryToParent()
    console.log("B00. 将目录容器添加到页面");
    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.gizmo\\:text-gizmo-gray-600');

    userMessages.forEach((msg, i) => {
        const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';
        const directoryEntry = document.createElement('div');
        directoryEntry.innerText = `${i + 1}. ${text}`;

        directoryEntry.setAttribute('data-index', i);  // 为每个目录项添加一个自定义属性,用于标识对应的用户消息的索引

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            // 找到父元素
            const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
            // 从父元素找到实际的滚动子元素(这里假设它是第一个子元素)
            const actualScrollContainerChild = actualScrollContainer ? actualScrollContainer.children[0] : null;
            // 模拟微小的滚动
            if (actualScrollContainerChild) {
                const originalScrollTop = actualScrollContainerChild.scrollTop;
                actualScrollContainerChild.scrollTop = originalScrollTop - 1;
            }

            // 延迟一小段时间后进行跳转
            setTimeout(() => {
                const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
                if (grandGrandParent) {
                    const parentOfGrandGrandParent = grandGrandParent.parentElement;
                    if (parentOfGrandGrandParent) {
                        parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                    }

                    // 跳转到对应位置,有缓动效果
                    placeholder.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});

                    setTimeout(() => {
                        placeholder.remove();
                    }, 0);  // 延迟500毫秒以确保滚动操作完成
                }
            }, 0); // 延迟200毫秒

        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        

        directoryDiv.appendChild(directoryEntry);
    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    
    appendDirectoryToParent()

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
    ScrollEventListener() 
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

// F01. 基于当前页面展示的内容,来让菜单对应目录部分高亮展示。 以下是代码:
function highlightDirectoryEntry() {
    const userMessages = document.querySelectorAll('.gizmo\\:text-gizmo-gray-600');
    let closestToTop = null;
    let minTop = Infinity;

    userMessages.forEach((msg, i) => {
        const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
        if (grandGrandParent) {
            const rect = grandGrandParent.getBoundingClientRect();
            const directoryEntry = document.querySelector(`[data-index="${i}"]`);

            // 移除所有高亮
            if (directoryEntry) {
                directoryEntry.classList.remove('highlight');
            }

            // 记录最接近顶部但仍在视窗内的元素
            if (rect.top >= 0 && rect.top <= window.innerHeight) {  // 修改这里,只检查顶部是否在视窗内
                if (rect.top < minTop) {
                    minTop = rect.top;
                    closestToTop = directoryEntry;
                }
            }
        }
    });

    // 只高亮最接近顶部的元素
    if (closestToTop) {
        closestToTop.classList.add('highlight');
    }
}

// F02. 监听滚动事件
function ScrollEventListener() {
// 找到实际的滚动容器
    const actualScrollContainer = document.querySelector('div[class^="react-scroll-to-bottom--css-"][class*="dark:bg-gray-800"]');
    // 绑定滚动事件监听器到实际的滚动容器
    if (actualScrollContainer) {
        console.log("F02 找到了实际的滚动容器");
        actualScrollContainer.addEventListener('scroll', highlightDirectoryEntry, true);
    }
}

// G01. 在页面加载完成后运行初始化函数

// 初始化后
initDirectory();
// highlightDirectoryEntry();

observeButtons();
console.log("E01. 加载完成后的初始化");
regenerateDirectory();
ScrollEventListener()

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮
- 知识目录高亮 
- 修了个bug,底部也可以用了 2023-10-15 00:47:48

*/

GPT历史目录 - 2023-10-12

// ==UserScript==
// @name         GPT History
// @namespace    
// @version      0.1
// @description
// @author       ttmouse & GPT-4
// @match        https://chat.openai.com/*
// @grant        none
// ==/UserScript==

// 我希望给GPT做一个目录。用js实现,在chrome中用console测试。
// 效果如下:
// 1.和GPT聊天的过程中,我发出去的内容会记录在在目录中,
// 2. 因为我发出去的内容可能会很长,在目录中进需要截取我发送内容的前10个字
// 3. 随着和GPT持续聊天,目录会自动更新
// 4. 目录支持点击跳转到对应的位置。
// 5. 目录有序号。
// 6. 打开已有聊天记录的页面,需要可以生成目录。
// 7. 用户有可能会修改历史聊天记录,目录需要重新生成。

////////////////////////// CSS //////////////////////////
const style = `

#directory {
    position: absolute;
    top: 81px;
    left: 10px;
    background-color: #7575751a;
    color: #676767;
    border-radius: 5px;
    padding: 5px;
    width: fit-content;
    font-size: 15px;
    display: inline-block;
}
#directoryTitle {
    font-weight: 700;
}
.placeholder {
    height: 70px;  /* 你想要保留的顶部距离 */
    visibility: hidden;
}

`;

const styleElement = document.createElement('style');
styleElement.innerHTML = style;
document.head.appendChild(styleElement);

////////////////////////// JS //////////////////////////
let isMouseDown = false;
let isDragging = false;
let mouseDownTimer;

// A01. 创建并设置目录容器
console.log("A01. 创建目录容器");
const directoryDiv = document.createElement('div');
directoryDiv.id = 'directory';
directoryDiv.classList.add('directory'); // 添加CSS类

let index = 1;  // 用于生成目录项的序号

// A02. 获取并设置聊天窗口的名称
function setChatTitle() {
    console.log("A02. 获取聊天窗口的名称");
    // 查找包含编辑和删除按钮的元素
    const activeChatElement = document.querySelector('div.absolute.flex.right-1.z-10.dark\\:text-gray-300.text-gray-800.visible');

    // 确保找到了该元素
    if (activeChatElement) {
        // 寻找包含标题文本的祖先元素
        const chatTitleElement = activeChatElement.closest('a').querySelector('.flex-1.text-ellipsis.max-h-5.overflow-hidden.break-all.relative');

        const chatTitle = chatTitleElement ? chatTitleElement.innerText : 'Chat';
        directoryTitle.innerText = `${chatTitle}`;
        directoryTitle.id = 'directoryTitle';
        directoryTitle.classList.add('directoryTitle'); // 添加CSS类
    }
}

// A03. 创建目录标题并添加拖动功能
const directoryTitle = document.createElement('div');
setChatTitle();

directoryTitle.addEventListener('mousedown', function(event) {
    isMouseDown = true;

    mouseDownTimer = setTimeout(() => {
        if (isMouseDown) {
            isDragging = true;

            const offsetX = event.clientX - directoryDiv.offsetLeft;
            const offsetY = event.clientY - directoryDiv.offsetTop;

            document.addEventListener('mousemove', function(event) {
                if (isDragging) {
                    directoryDiv.style.left = `${event.clientX - offsetX}px`;
                    directoryDiv.style.top = `${event.clientY - offsetY}px`;
                }
            });
        }
    }, 150);
});

document.addEventListener('mouseup', () => {
    isMouseDown = false;
    isDragging = false;
    clearTimeout(mouseDownTimer);
});

// A04. 将新创建的元素添加到页面的body(或其他适当的位置)
// 找到父容器
const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
// 将新创建的DIV添加到父容器之下
parentContainer.appendChild(directoryDiv);
// 添加到页面
console.log("A04. 将目录容器添加到页面");

// B01. 生成目录
function initDirectory() {

    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    // 将新创建的DIV添加到父容器之下
    parentContainer.appendChild(directoryDiv);
    console.log("B00. 将目录容器添加到页面");

    console.log("B01. 开始生成目录");
    const userMessages = document.querySelectorAll('.gizmo\\:text-gizmo-gray-600');
    userMessages.forEach((msg, i) => {
        const text = msg ? msg.innerText.split('\n')[0].substring(0, 10) : '';
        const directoryEntry = document.createElement('div');
        directoryEntry.innerText = `${i + 1}. ${text}`;

        // 希望点击之后,页面滚动到对应的用户消息,且不会被顶部的菜单栏遮挡,下移70px

        const placeholder = document.createElement('div');
        placeholder.className = 'placeholder';

        // B01.1 点击目录项,页面滚动到对应的用户消息
        directoryEntry.addEventListener('click', () => {

            const grandGrandParent = msg.parentElement && msg.parentElement.parentElement && msg.parentElement.parentElement.parentElement;
            if (grandGrandParent) {
                const parentOfGrandGrandParent = grandGrandParent.parentElement;
                if (parentOfGrandGrandParent) {
                    parentOfGrandGrandParent.insertBefore(placeholder, grandGrandParent);
                }
                placeholder.scrollIntoView();

                setTimeout(() => {
                    placeholder.remove();
                }, 50);  // 延迟500毫秒以确保滚动操作完成

            }
        });

        // B01.2 双击目录项,页面滚动到对应的用户消息,并定位到编辑按钮
        directoryEntry.addEventListener('dblclick', () => {
            // 找到目标按钮
            const parentOfMsg = msg.parentElement;
            if (parentOfMsg) {
                const secondChild = parentOfMsg.children[1];  // 注意这里我改成了1,因为数组索引是从0开始的
                if (secondChild) {
                    const editButton = secondChild.querySelector('button');  // 假设按钮是该元素的第一个子元素
                    if (editButton) {
                        // 触发按钮的点击事件
                        editButton.click();

                        // 获取 msg 的第一个子元素
                        const firstChild = msg.children && msg.children[0];

                        if (firstChild) {
                            // 在第一个子元素里查找更深一层的子元素
                            const deeperDiv = firstChild.querySelector('div');

                            if (deeperDiv) {
                                // 在更深一层的子元素里查找 textarea
                                const textareaElement = deeperDiv.querySelector('textarea');
                                if (textareaElement) {
                                    console.log("调试信息: 找到 textarea 并设置光标");
                                    
                                    // 设置光标到 textarea 文本的末尾
                                    textareaElement.focus();
                                    // textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length); // 设置光标到文本末尾

                                    // 选择文本框内的所有文本
                                    textareaElement.select();
                                    // textareaElement.setSelectionRange(0, textareaElement.value.length); // 选择文本框内的所有文本

                                } else {
                                    console.log("调试信息: 没有找到对应的 textarea");
                                }
                            } else {
                                console.log("调试信息: 没有找到第一个子元素");
                            }
                            
                        }
                    }
                }
            }

        });

        directoryDiv.appendChild(directoryEntry);
    });
    index = userMessages.length + 1;
    console.log("B01. 初始化目录完成");
}

// B02. 重新生成整个目录
function regenerateDirectory() {
    console.log("B02. 设置重新生成目录函数");
    // 找到父容器
    const parentContainer = document.querySelector('.relative.flex.h-full.max-w-full.flex-1.overflow-hidden');
    // 将新创建的DIV添加到父容器之下
    parentContainer.appendChild(directoryDiv);

    // 清空现有目录
    while (directoryDiv.firstChild) {
        directoryDiv.removeChild(directoryDiv.firstChild);
    }
    // 重新设置聊天窗口名称
    setChatTitle();
    // 重新添加标题
    directoryDiv.appendChild(directoryTitle);
    // 重新生成目录
    initDirectory();
    console.log("B02. 重新生成目录完成");
    observeButtons();
}

// D01. 监听性能条目
const observer = new PerformanceObserver((list) => {

    for (const entry of list.getEntries()) {
        if (entry.name.includes('https://chat.openai.com/backend-api/conversation')) {
            console.log("D02. 检测到内容更新");

            let intervalId = setInterval(() => {
                // D03. 检测到聊天内容更新后,识别share chat按钮是否存在,如果存在,则重新生成目录
                const shareButton = document.querySelector('button[aria-label="Share chat"]');

                if (shareButton) {
                    regenerateDirectory();
                    clearInterval(intervalId); // 停止定时任务
                }
            }, 50);
        }
    }
});

observer.observe({ entryTypes: ['resource'] });

// E01. 对左右箭头按钮添加点击事件
function observeButtons() {
    const allButtonElements = document.querySelectorAll('button');
    allButtonElements.forEach(buttonElement => {
        const svgElement = buttonElement.querySelector('svg');
        if (svgElement) {
            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);
                    // 点击1000毫秒之后,重新添加左右箭头按钮的点击事件
                    console.log("E01. 重新添加左右箭头按钮的点击事件");
                });
            }
        }
    });
}

observeButtons();

// E01. 在页面加载完成后运行初始化函数
console.log("E01. 加载完成后的初始化");
regenerateDirectory();

// 更新日志:
/* 
- 只是目录双击之后,页面滚动到对应的用户消息,并定位到编辑按钮

*/