您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为ChatGPT对话生成右侧大纲视图,提取问题前10个字作为标题
当前为
// ==UserScript== // @name ChatGPT 大纲生成器 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 为ChatGPT对话生成右侧大纲视图,提取问题前10个字作为标题 // @author Y.V // @license AGPL-3.0-or-later // @match https://chatgpt.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com // @grant none // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function () { 'use strict'; /** * ChatGPT大纲生成器类 */ class ChatGPTOutlineGenerator { constructor() { this.outlineContainer = null; this.toggleButton = null; this.styleElement = null; this.cssStyles = ` .outline-container { position: fixed; top: 70px; right: 20px; width: 280px; max-height: calc(100vh - 100px); background-color: rgba(247, 247, 248, 0.85); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); z-index: 1000; overflow-y: auto; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); font-family: 'Söhne', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif; opacity: 0.95; backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); } .outline-container:hover { opacity: 1; background-color: rgba(247, 247, 248, 0.98); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); transform: translateY(-2px); } .dark .outline-container { background-color: rgba(52, 53, 65, 0.85); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); } .dark .outline-container:hover { background-color: rgba(52, 53, 65, 0.98); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); } .outline-header { padding: 16px; font-weight: 600; font-size: 16px; border-bottom: 1px solid rgba(0, 0, 0, 0.05); display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: inherit; border-radius: 12px 12px 0 0; z-index: 2; } .dark .outline-header { border-bottom: 1px solid rgba(255, 255, 255, 0.05); color: #ececf1; } .outline-title { display: flex; align-items: center; gap: 8px; } .outline-title-icon { color: #10a37f; } .outline-items { padding: 8px 0; } .outline-item { padding: 10px 16px; cursor: pointer; transition: all 0.2s ease; font-size: 14px; border-left: 3px solid transparent; display: flex; align-items: center; margin: 2px 0; border-radius: 0 4px 4px 0; } .outline-item:hover { background-color: rgba(0, 0, 0, 0.05); transform: translateX(2px); } .dark .outline-item:hover { background-color: rgba(255, 255, 255, 0.05); } .outline-item.active { border-left-color: #10a37f; background-color: rgba(16, 163, 127, 0.1); font-weight: 500; } .outline-item-icon { margin-right: 10px; color: #10a37f; transition: transform 0.2s ease; } .outline-item:hover .outline-item-icon { transform: scale(1.1); } .outline-item-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; line-height: 1.4; } .dark .outline-item-text { color: #ececf1; } .outline-toggle { position: fixed; top: 70px; right: 20px; width: 42px; height: 42px; border-radius: 50%; background-color: rgba(16, 163, 127, 0.9); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 1001; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .outline-toggle:hover { transform: scale(1.08); background-color: #10a37f; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .outline-toggle svg { width: 20px; height: 20px; transition: transform 0.3s ease; } .outline-toggle:hover svg { transform: rotate(90deg); } .outline-close { cursor: pointer; opacity: 0.7; transition: all 0.2s ease; padding: 4px; border-radius: 4px; } .outline-close:hover { opacity: 1; background-color: rgba(0, 0, 0, 0.05); } .dark .outline-close:hover { background-color: rgba(255, 255, 255, 0.1); } .outline-empty { padding: 20px 16px; text-align: center; color: #888; font-style: italic; font-size: 14px; } .dark .outline-empty { color: #aaa; } @media (max-width: 1400px) { .outline-container { width: 250px; } } @media (max-width: 1200px) { .outline-container { width: 220px; } } @media (max-width: 768px) { .outline-container { display: none; } }`; } /** * 初始化大纲生成器 */ init() { // 等待页面加载完成 if (!document.querySelector('main')) { setTimeout(() => this.init(), 1000); return; } this.addStyles(); this.outlineContainer = this.createOutlineContainer(); this.toggleButton = this.createToggleButton(); // 初始化大纲 setTimeout(() => this.generateOutlineItems(), 1000); // 设置初始暗黑模式状态 this.outlineContainer.classList.toggle('dark', this.detectDarkMode()); // 监听暗黑模式变化 this.observeDarkModeChanges(); // 监听新消息 this.observeNewMessages(); // 监听滚动以高亮当前可见的消息 this.observeScroll(); } /** * 添加样式到页面 */ addStyles() { this.styleElement = document.createElement('style'); this.styleElement.textContent = this.cssStyles; document.head.appendChild(this.styleElement); } /** * 创建大纲容器 * @returns {HTMLElement} 大纲容器元素 */ createOutlineContainer() { const container = document.createElement('div'); container.className = 'outline-container'; container.innerHTML = ` <div class="outline-header"> <div class="outline-title"> <span class="outline-title-icon"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M4 6H20M4 12H20M4 18H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </span> <span>对话大纲</span> </div> <span class="outline-close"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </span> </div> <div class="outline-items"></div>`; document.body.appendChild(container); // 添加关闭事件 container.querySelector('.outline-close').addEventListener('click', () => { container.style.display = 'none'; this.toggleButton.style.display = 'flex'; }); return container; } /** * 创建切换按钮 * @returns {HTMLElement} 切换按钮元素 */ createToggleButton() { const button = document.createElement('div'); button.className = 'outline-toggle'; button.innerHTML = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M4 6H20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M4 12H20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M4 18H20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> </svg>`; document.body.appendChild(button); // 添加点击事件 button.addEventListener('click', () => { this.outlineContainer.style.display = 'block'; button.style.display = 'none'; // 重新生成大纲,确保最新状态 this.generateOutlineItems(); }); return button; } /** * 提取问题文本的前16个字符 * @param {string} text 问题文本 * @returns {string} 提取后的标题 */ extractQuestionTitle(text) { // 去除空白字符 const trimmed = text.trim(); // 如果文本为空,返回默认文本 if (!trimmed) return "空白问题"; // 提取前16个字符,如果不足16个则全部返回 return trimmed.length > 16 ? trimmed.substring(0, 16) + '...' : trimmed; } /** * 生成大纲项 */ generateOutlineItems() { const outlineItems = this.outlineContainer.querySelector('.outline-items'); outlineItems.innerHTML = ''; // 获取所有用户消息 const userMessages = document.querySelectorAll('[data-message-author-role="user"]'); if (userMessages.length === 0) { outlineItems.innerHTML = '<div class="outline-empty">暂无对话内容</div>'; return; } userMessages.forEach((message, index) => { const messageText = message.querySelector('.whitespace-pre-wrap')?.textContent || ''; const title = this.extractQuestionTitle(messageText); const item = document.createElement('div'); item.className = 'outline-item'; item.dataset.index = index; item.dataset.messageId = message.id || `message-${index}`; item.innerHTML = ` <span class="outline-item-icon"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" fill="currentColor"/> </svg> </span> <span class="outline-item-text">${index + 1}. ${title}</span>`; // 添加点击事件 item.addEventListener('click', () => this.handleItemClick(item, message)); outlineItems.appendChild(item); }); // 检查是否有可见的消息,并高亮对应的大纲项 this.highlightVisibleItem(); } /** * 处理大纲项点击事件 * @param {HTMLElement} item 点击的大纲项 * @param {HTMLElement} message 对应的消息元素 */ handleItemClick(item, message) { // 滚动到对应的消息 message.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 高亮当前项 this.highlightItem(item); // 添加临时高亮效果 message.style.transition = 'background-color 0.5s'; message.style.backgroundColor = 'rgba(16, 163, 127, 0.1)'; setTimeout(() => { message.style.backgroundColor = ''; }, 1500); } /** * 高亮指定的大纲项 * @param {HTMLElement} item 要高亮的大纲项 */ highlightItem(item) { document.querySelectorAll('.outline-item').forEach(el => { el.classList.remove('active'); }); item.classList.add('active'); } /** * 监听页面滚动,高亮当前可见的消息对应的大纲项 */ observeScroll() { let scrollTimer = null; window.addEventListener('scroll', () => { // 使用防抖技术减少滚动事件处理频率 if (scrollTimer) clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { this.highlightVisibleItem(); }, 100); }); } /** * 高亮当前可见的消息对应的大纲项 */ highlightVisibleItem() { const userMessages = document.querySelectorAll('[data-message-author-role="user"]'); if (!userMessages.length) return; // 找到当前视口中最靠近顶部的消息 let closestMessage = null; let closestDistance = Infinity; const viewportHeight = window.innerHeight; const viewportMiddle = viewportHeight / 2; userMessages.forEach(message => { const rect = message.getBoundingClientRect(); // 计算消息中心点到视口中心的距离 const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportMiddle); // 如果消息在视口内且距离更近 if (rect.top < viewportHeight && rect.bottom > 0 && distance < closestDistance) { closestDistance = distance; closestMessage = message; } }); if (closestMessage) { // 找到对应的大纲项并高亮 const index = Array.from(userMessages).indexOf(closestMessage); const outlineItem = this.outlineContainer.querySelector(`.outline-item[data-index="${index}"]`); if (outlineItem) { this.highlightItem(outlineItem); } } } /** * 检测暗黑模式 * @returns {boolean} 是否为暗黑模式 */ detectDarkMode() { return document.documentElement.classList.contains('dark'); } /** * 监听暗黑模式变化 */ observeDarkModeChanges() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { const isDarkMode = this.detectDarkMode(); this.outlineContainer.classList.toggle('dark', isDarkMode); } }); }); observer.observe(document.documentElement, { attributes: true }); } /** * 监听新消息 */ observeNewMessages() { const observer = new MutationObserver((mutations) => { let shouldUpdate = false; mutations.forEach(mutation => { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && (node.querySelector('[data-message-author-role="user"]') || node.hasAttribute && node.hasAttribute('data-message-author-role'))) { shouldUpdate = true; break; } } } }); if (shouldUpdate) { setTimeout(() => this.generateOutlineItems(), 500); // 延迟执行,确保DOM已更新 } }); // 监听整个聊天容器 const chatContainer = document.querySelector('main'); if (chatContainer) { observer.observe(chatContainer, { childList: true, subtree: true }); } } } /** * 插件管理器类 - 用于管理多个插件 */ class PluginManager { constructor() { this.plugins = []; } /** * 注册插件 * @param {Object} plugin 插件实例 */ register(plugin) { this.plugins.push(plugin); return this; } /** * 初始化所有插件 */ initAll() { this.plugins.forEach(plugin => { if (typeof plugin.init === 'function') { setTimeout(() => plugin.init(), 1500); // 延迟启动,确保页面已加载 } }); } } /** * 创建并启动插件管理器 */ $(document).ready(() => { // 创建插件管理器 const pluginManager = new PluginManager(); // 注册大纲生成器插件 pluginManager.register(new ChatGPTOutlineGenerator()); // 初始化所有插件 pluginManager.initAll(); }); })();