// ==UserScript==
// @name ChatGPT体验增强插件/全局与单条导出Markdown/美化对话列表面板
// @namespace https://fcai.me/afdian
// @version 1.1
// @description ChatGPT官网 时间线/预览/跳转/长按标记/搜索/公式复制/全局与单条导出Markdown。增强稳定性与兼容性(修复长对话虚拟化节点消失,优化导出无冗余引用胶囊,完美修复公式多余转义字符,统一行内/行间公式嗅探复制交互,美化对话列表面板)
// @author Kongkon
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @match https://new.oaifree.com/**
// @match https://share.github.cn.com/**
// @match https://chatgpt.com/**
// @match https://*.oaifree.com/**
// @match https://cc.plusai.me/**
// @match https://chat.chatgptplus.cn/**
// @match https://chat.rawchat.cc/**
// @match https://chat.sharedchat.cn/**
// @match https://chat.gptdsb.com/**
// @match https://chat.freegpts.org/**
// @match https://gpt.github.cn.com/**
// @match https://chat.aicnn.xyz/**
// @match https://*.xyhelper.com.cn/**
// @match https://oai.aitopk.com/**
// @match https://www.opkfc.com/**
// @match https://bus.hematown.com/**
// @match https://chatgpt.dairoot.cn/**
// @match https://plus.aivvm.com/**
// @match https://sharechat.aischat.xyz/**
// @match https://web.tu-zi.com/**
// @match https://chat1.2233.ai/**
// @match https://2233.ai/**
// @match https://yesiamai.com/**
// @match https://www.azs.ai/**
// @match https://sass-node6.chatshare.biz/**
// @match https://sass-node1.chatshare.biz/**
// @match https://sass-node2.chatshare.biz/**
// @match https://sass-node3.chatshare.biz/**
// @match https://sass-node4.chatshare.biz/**
// @match https://sass-node5.chatshare.biz/**
// @match https://oai.253282.xyz/**
// @match https://gpt.universalbus.cn/**
// @match https://go.gptdsb.com/**
// @match https://*.azs.ai/**
// @match https://azs.ai/**
// @match https://*.azs.ai/c/**
// @license CC-BY-NC-4.0
// @grant GM_addStyle
// @require https://unpkg.com/turndown/lib/turndown.browser.umd.js
// @require https://unpkg.com/turndown-plugin-gfm/dist/turndown-plugin-gfm.js
// @run-at document-idle
// @noframes
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 1. 注入 CSS 样式(对话列表面板已全面美化)
// ==========================================
GM_addStyle(`
:root {
--tl-capsule-bg: #f7f9fa;
--tl-capsule-border: #e5e8eb;
--tl-capsule-shadow: rgba(0, 0, 0, 0.05);
--tl-dot-color: #c5cbd5;
--tl-dot-active-ring: #508cf3;
--tl-tooltip-bg: rgba(28, 28, 30, 0.95);
--tl-tooltip-text: #ffffff;
--tl-tooltip-shadow: rgba(0, 0, 0, 0.15);
--tl-panel-bg: rgba(255, 255, 255, 0.90);
--tl-panel-text: #1c1c1e;
--tl-input-bg: rgba(0, 0, 0, 0.05);
--tl-item-hover: rgba(0, 0, 0, 0.04);
--tl-icon-color: #565869;
--tl-highlight-bg: rgba(255, 193, 7, 0.12);
--tl-index-bg: rgba(0, 0, 0, 0.06);
}
html.dark {
--tl-capsule-bg: rgba(45, 45, 45, 0.85);
--tl-capsule-border: rgba(255, 255, 255, 0.08);
--tl-capsule-shadow: rgba(0, 0, 0, 0.3);
--tl-dot-color: #565869;
--tl-dot-active-ring: #508cf3;
--tl-tooltip-bg: rgba(236, 236, 236, 0.95);
--tl-tooltip-text: #1c1c1e;
--tl-tooltip-shadow: rgba(0, 0, 0, 0.3);
--tl-panel-bg: rgba(32, 33, 35, 0.92);
--tl-panel-text: #ececec;
--tl-input-bg: rgba(255, 255, 255, 0.1);
--tl-item-hover: rgba(255, 255, 255, 0.06);
--tl-icon-color: #c5cbd5;
--tl-highlight-bg: rgba(255, 193, 7, 0.18);
--tl-index-bg: rgba(255, 255, 255, 0.08);
}
#chatgpt-dot-timeline {
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
width: 36px;
background: var(--tl-capsule-bg);
border: 1px solid var(--tl-capsule-border);
border-radius: 20px;
padding: 18px 0;
display: none;
flex-direction: column;
align-items: center;
gap: 20px;
z-index: 99998;
box-shadow: 0 4px 12px var(--tl-capsule-shadow);
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
#chatgpt-dot-timeline::-webkit-scrollbar { display: none; }
#chatgpt-timeline-indicator {
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
box-shadow: 0 0 0 3px var(--tl-dot-active-ring);
pointer-events: none;
z-index: 10;
top: 0;
left: 10px;
opacity: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, opacity 0.3s ease;
}
.timeline-dot {
width: 14px;
height: 14px;
border-radius: 50%;
background-color: var(--tl-dot-color);
cursor: pointer;
position: relative;
flex-shrink: 0;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-user-select: none;
user-select: none;
}
.timeline-dot.highlighted { background-color: #FFC107 !important; }
.timeline-dot.highlighted:hover { box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.5); }
#chatgpt-timeline-tooltip {
position: fixed;
background: var(--tl-tooltip-bg);
color: var(--tl-tooltip-text);
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
width: max-content;
max-width: 300px;
word-break: break-word;
white-space: pre-wrap;
pointer-events: none;
z-index: 99999;
opacity: 0;
transform: translateY(-50%) translateX(8px);
transition: opacity 0.2s ease, transform 0.2s ease;
box-shadow: 0 4px 16px var(--tl-tooltip-shadow);
backdrop-filter: blur(8px);
}
#chatgpt-timeline-tooltip.visible { opacity: 1; transform: translateY(-50%) translateX(0); }
#chatgpt-timeline-tooltip::after {
content: '';
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%);
border-width: 6px 0 6px 6px;
border-style: solid;
border-color: transparent transparent transparent var(--tl-tooltip-bg);
}
/* 工具栏配置 */
#chatgpt-timeline-tools {
position: fixed;
top: 50%;
right: 66px;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 12px;
z-index: 99998;
}
.tool-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--tl-capsule-bg);
border: 1px solid var(--tl-capsule-border);
box-shadow: 0 4px 12px var(--tl-capsule-shadow);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--tl-icon-color);
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.tool-btn:hover { color: var(--tl-dot-active-ring); }
#menu-btn { display: none; }
/* ================= 对话列表面板(全面美化) ================= */
#chatgpt-timeline-panel {
position: fixed;
top: 50%;
right: 112px;
transform: translateY(-50%) translateX(20px);
width: 320px;
max-height: 75vh;
background: var(--tl-panel-bg);
border: 1px solid var(--tl-capsule-border);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0,0,0,0.04);
z-index: 99997;
display: flex;
flex-direction: column;
opacity: 0;
pointer-events: none;
visibility: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
#chatgpt-timeline-panel.visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(-50%) translateX(0);
}
#chatgpt-panel-header {
padding: 14px 16px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--tl-capsule-border);
user-select: none;
}
#chatgpt-panel-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--tl-panel-text);
letter-spacing: -0.01em;
}
#chatgpt-panel-close {
background: none;
border: none;
color: var(--tl-icon-color);
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
width: 28px;
height: 28px;
}
#chatgpt-panel-close:hover {
background: rgba(235, 87, 87, 0.15);
color: #eb5757;
}
#chatgpt-panel-search-wrapper {
padding: 10px 14px;
position: relative;
border-bottom: 1px solid var(--tl-capsule-border);
}
.search-icon {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
color: var(--tl-icon-color);
pointer-events: none;
opacity: 0.6;
}
#chatgpt-panel-search {
width: 100%;
background: var(--tl-input-bg);
border: 1px solid transparent;
border-radius: 10px;
padding: 9px 14px 9px 38px;
color: var(--tl-panel-text);
font-size: 13px;
outline: none;
box-sizing: border-box;
transition: border-color 0.25s, box-shadow 0.25s;
}
#chatgpt-panel-search::placeholder {
color: var(--tl-icon-color);
opacity: 0.7;
}
#chatgpt-panel-search:focus {
border-color: var(--tl-dot-active-ring);
box-shadow: 0 0 0 3px rgba(80, 140, 243, 0.15);
}
#chatgpt-panel-list {
flex-grow: 1;
overflow-y: auto;
padding: 8px 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
#chatgpt-panel-list::-webkit-scrollbar { width: 5px; }
#chatgpt-panel-list::-webkit-scrollbar-thumb {
background: var(--tl-input-bg);
border-radius: 3px;
}
#chatgpt-panel-list::-webkit-scrollbar-track { background: transparent; }
.panel-list-item {
padding: 10px 12px;
border-radius: 12px;
cursor: pointer;
color: var(--tl-panel-text);
font-size: 13px;
line-height: 1.5;
display: flex;
align-items: flex-start;
gap: 12px;
transition: background 0.2s, box-shadow 0.2s;
margin: 0 2px;
position: relative;
}
.panel-list-item:hover {
background: var(--tl-item-hover);
}
.panel-list-item.active {
background: var(--tl-item-hover);
box-shadow: inset 3px 0 0 var(--tl-dot-active-ring);
}
.panel-list-item.highlighted {
background: var(--tl-highlight-bg);
}
.panel-list-index {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--tl-index-bg);
color: var(--tl-icon-color);
font-weight: 700;
font-size: 12px;
flex-shrink: 0;
margin-top: 1px;
transition: background 0.2s, color 0.2s, transform 0.2s;
}
.panel-list-item.active .panel-list-index {
background: var(--tl-dot-active-ring);
color: #fff;
transform: scale(1.05);
}
.panel-list-item.highlighted .panel-list-index {
background: #FFC107;
color: #000;
}
.panel-list-text {
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
min-width: 0;
}
/* 行内与行间公式交互样式统一 */
.katex {
cursor: pointer;
transition: opacity 0.2s ease, background-color 0.2s ease;
border-radius: 4px;
}
.katex:hover {
opacity: 0.85;
}
.katex:active {
opacity: 0.6;
}
/* 复制成功Toast样式 */
#formula-copy-toast {
position: fixed;
background: rgba(16, 163, 127, 0.95);
color: #fff;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
pointer-events: none;
z-index: 100000;
opacity: 0;
transform: translate(-50%, 0);
transition: opacity 0.2s ease, transform 0.2s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 4px;
backdrop-filter: blur(4px);
}
#formula-copy-toast.show {
opacity: 1;
transform: translate(-50%, -20px);
}
`);
// ==========================================
// 2. 构建 DOM 与 状态缓存
// ==========================================
const container = document.createElement('div');
container.id = 'chatgpt-dot-timeline';
document.body.appendChild(container);
const activeIndicator = document.createElement('div');
activeIndicator.id = 'chatgpt-timeline-indicator';
container.appendChild(activeIndicator);
const globalTooltip = document.createElement('div');
globalTooltip.id = 'chatgpt-timeline-tooltip';
document.body.appendChild(globalTooltip);
const toolsContainer = document.createElement('div');
toolsContainer.id = 'chatgpt-timeline-tools';
document.body.appendChild(toolsContainer);
const menuBtn = document.createElement('div');
menuBtn.id = 'menu-btn';
menuBtn.className = 'tool-btn';
menuBtn.title = '打开对话列表';
menuBtn.innerHTML = ``;
toolsContainer.appendChild(menuBtn);
const thinkBtn = document.createElement('div');
thinkBtn.id = 'think-btn';
thinkBtn.className = 'tool-btn';
thinkBtn.title = '开启深度思考 (/think)';
thinkBtn.innerHTML = ``;
toolsContainer.appendChild(thinkBtn);
// 新增:全局聊天导出按钮
const exportGlobalBtn = document.createElement('div');
exportGlobalBtn.id = 'export-global-btn';
exportGlobalBtn.className = 'tool-btn';
exportGlobalBtn.title = '全局聊天导出 (/export)';
exportGlobalBtn.innerHTML = ``;
toolsContainer.appendChild(exportGlobalBtn);
exportGlobalBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await exportGlobalChat();
});
// ====== 带有标题栏和关闭按钮的面板 ======
const panel = document.createElement('div');
panel.id = 'chatgpt-timeline-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
const searchInput = document.getElementById('chatgpt-panel-search');
const panelList = document.getElementById('chatgpt-panel-list');
const closeBtn = document.getElementById('chatgpt-panel-close');
// 全局复制Toast提示框
const formulaToast = document.createElement('div');
formulaToast.id = 'formula-copy-toast';
formulaToast.innerHTML = `复制成功`;
document.body.appendChild(formulaToast);
// 全局文本缓存
const turnTextCache = new Map();
// ==========================================
// 3. 逻辑引擎与Markdown导出集成
// ==========================================
let isAutoScrolling = false;
let scrollEndTimer = null;
let tdService = null;
// 初始化Turndown引擎
function getTurndownService() {
if (tdService) return tdService;
tdService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*'
});
if (typeof turndownPluginGfm !== 'undefined') {
tdService.use(turndownPluginGfm.gfm);
}
tdService.addRule('strikethrough-fix', {
filter:['del', 's', 'strike'],
replacement: function (content) {
return '~~' + content + '~~';
}
});
// 自定义解析ChatGPT特有代码块
tdService.addRule('chatgptCodeBlock', {
filter: 'pre',
replacement: function (content, node) {
let lang = '';
const langDiv = node.querySelector('.text-token-text-primary');
if (langDiv) lang = langDiv.textContent.trim();
let codeText = '';
const contentDiv = node.querySelector('.cm-content');
if (contentDiv) {
const clone = contentDiv.cloneNode(true);
clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
codeText = clone.textContent;
} else {
codeText = node.textContent;
}
return '\n```' + lang + '\n' + codeText + '\n```\n';
}
});
// 完美解析公式,防止多余转义符
tdService.addRule('chatgptMath', {
filter: function(node) {
if (!node.classList) return false;
if (node.classList.contains('katex-display')) return true;
if (node.classList.contains('katex') && (!node.parentElement || !node.parentElement.classList.contains('katex-display'))) return true;
return false;
},
replacement: function(content, node) {
const annotation = node.querySelector('annotation[encoding="application/x-tex"]');
if (!annotation) return content;
const tex = annotation.textContent.trim();
if (node.classList.contains('katex-display')) {
return '\n$$\n' + tex + '\n$$\n';
} else {
return '$' + tex + '$';
}
}
});
return tdService;
}
// 统一复用的核心提取逻辑(抽取自原单个回答提取逻辑)
function getAssistantTurnMarkdown(turnNode) {
const contentContainer = turnNode.querySelector('.flex.max-w-full.flex-col.gap-4.grow');
// 兼容不同的DOM层级嵌套
if (!contentContainer) {
const mdNode = turnNode.querySelector('.markdown');
if (mdNode) {
const turndown = getTurndownService();
const clone = mdNode.cloneNode(true);
clone.querySelectorAll('svg').forEach(svg => svg.remove());
return turndown.turndown(clone).trim();
}
return '';
}
let mdContent = "";
const children = contentContainer.children;
const turndown = getTurndownService();
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.getAttribute('data-message-author-role') === 'assistant') {
const markdownDiv = child.querySelector('.markdown');
if (markdownDiv) {
const nextChild = children[i+1];
let isThought = false;
let thoughtTime = "Thought process";
if (nextChild && nextChild.classList.contains('justify-between')) {
const btn = nextChild.querySelector('button');
if (btn && btn.textContent.includes('Thought')) {
isThought = true;
thoughtTime = btn.textContent.trim();
}
}
const clone = markdownDiv.cloneNode(true);
// 擦除SVG与引用胶囊等冗余节点
clone.querySelectorAll('svg').forEach(svg => svg.remove());
clone.querySelectorAll('[data-testid="webpage-citation-pill"]').forEach(pill => {
const parent = pill.parentElement;
if (parent && parent.tagName.toLowerCase() === 'span' && parent.childElementCount === 1) {
parent.remove();
} else {
pill.remove();
}
});
clone.querySelectorAll('[aria-haspopup="dialog"]').forEach(el => el.remove());
let md = turndown.turndown(clone);
if (isThought) {
mdContent += `> **${thoughtTime}**\n>\n> ` + md.split('\n').join('\n> ') + `\n\n`;
} else {
mdContent += md + `\n\n`;
}
}
}
}
return mdContent.trim();
}
// 提取 User Turn 内容
function extractUserContent(turnNode) {
const userDiv = turnNode.querySelector('[data-message-author-role="user"]');
if (userDiv) {
return userDiv.innerText.trim();
}
return '';
}
// 格式化花边构造器
function formatTurn(turnIndex, userText, assistantText) {
return `╔═══════\n║ TURN #${turnIndex}\n╚═══════\nUser:\n${userText || '[未检测到文本或仅包含多媒体/附件]'}\n---\nChatGPT:\n${assistantText || ''}\n---\n`;
}
// ================== 全局导出新功能 ==================
async function exportGlobalChat() {
// 1. 创建毛玻璃全屏遮罩层防止用户操作
const mask = document.createElement('div');
mask.style.cssText = `
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
z-index: 999999; display: flex; flex-direction: column;
align-items: center; justify-content: center; color: #ffffff;
font-size: 16px; font-weight: 500; font-family: sans-serif;
pointer-events: auto; user-select: none;
`;
mask.innerHTML = `
正在准备拉取历史记录...
`;
document.body.appendChild(mask);
const statusEl = document.getElementById('global-export-status');
// 2. 动态寻找滚动容器并主动滑动到最顶端
let scrollRetries = 0;
const maxRetries = 40; // 设定重试阈值,足够应对上百轮的超级长对话
while (scrollRetries < maxRetries) {
const turns = document.querySelectorAll('section[data-turn]');
if (!turns.length) break;
const firstTurn = turns[0];
// 【核心优化】动态向上探测当前生效的滚动包裹层,不依赖固定类名
let scroller = firstTurn.parentElement;
let scrollContainer = window;
let isWindow = true;
while (scroller && scroller !== document.documentElement && scroller !== document.body) {
const style = window.getComputedStyle(scroller);
if ((style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflowY === 'overlay') && scroller.scrollHeight > scroller.clientHeight) {
scrollContainer = scroller;
isWindow = false;
break;
}
scroller = scroller.parentElement;
}
const prevFirstTurn = firstTurn;
// 【三重触发机制】确保万无一失拉到最顶
if (!isWindow) {
scrollContainer.scrollTop = 0;
}
window.scrollTo(0, 0);
firstTurn.scrollIntoView({ behavior: 'instant', block: 'start', inline: 'nearest' });
statusEl.textContent = `正在向上追溯历史记录... (第 ${scrollRetries + 1} 次翻页)`;
// 给网络请求和 React 渲染留出缓冲时间
await new Promise(r => setTimeout(r, 800));
const newTurns = document.querySelectorAll('section[data-turn]');
const newFirstTurn = newTurns.length ? newTurns[0] : null;
// 如果向上翻页后,顶层第一个对话节点仍是之前那个,说明很可能到底(顶)了
if (prevFirstTurn === newFirstTurn) {
// 防抖:网络波动可能导致加载延迟,再等一下进行二次验证
await new Promise(r => setTimeout(r, 1000));
const finalTurns = document.querySelectorAll('section[data-turn]');
if (prevFirstTurn === (finalTurns.length ? finalTurns[0] : null)) {
break; // 确实顶端了,跳出循环
}
}
scrollRetries++;
}
statusEl.textContent = "所有历史记录加载完毕,正在极速提取与排版...";
await new Promise(r => setTimeout(r, 300)); // 让界面重绘给用户直观反馈
// 3. 开始按序抓取DOM中呈现的对话组序列并匹配
const turnsList = document.querySelectorAll('section[data-turn]');
let markdownChunks =[];
let currentTurnNum = 1;
let currentUserText = '';
let currentAssistantText = '';
let hasUser = false;
for (let i = 0; i < turnsList.length; i++) {
const node = turnsList[i];
const role = node.getAttribute('data-turn');
if (role === 'user') {
// 如果已经搜集完了整整一回合却碰到了新的User,那么封包推入栈中再开始下一轮
if (hasUser || currentAssistantText) {
markdownChunks.push(formatTurn(currentTurnNum, currentUserText, currentAssistantText));
currentTurnNum++;
currentUserText = '';
currentAssistantText = '';
}
currentUserText = extractUserContent(node);
hasUser = true;
} else if (role === 'assistant') {
const astText = getAssistantTurnMarkdown(node);
// 处理Assistant长文本或是连续生成分叉文本的聚合情况
if (currentAssistantText) {
currentAssistantText += '\n\n' + astText;
} else {
currentAssistantText = astText;
}
}
}
// 将循环结束时遗留的最后一块压入最终组合栈
if (hasUser || currentAssistantText) {
markdownChunks.push(formatTurn(currentTurnNum, currentUserText, currentAssistantText));
}
// 4. 以双换行符拼接后导出流
const finalMarkdown = markdownChunks.join('\n');
if (finalMarkdown.trim().length > 0) {
const blob = new Blob([finalMarkdown], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ChatGPT_Global_Export_${new Date().toISOString().replace(/[:.]/g, '-')}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else {
alert('当前页面未能提取到任何合法对话内容!');
}
// 5. 关闭遮罩层
mask.remove();
}
// 面板打开关闭交互
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
panel.classList.toggle('visible');
if (panel.classList.contains('visible')) searchInput.focus();
});
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
panel.classList.remove('visible');
});
document.addEventListener('click', (e) => {
if (panel.classList.contains('visible') && !panel.contains(e.target) && !menuBtn.contains(e.target)) {
panel.classList.remove('visible');
}
});
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.toLowerCase().trim();
panelList.querySelectorAll('.panel-list-item').forEach(item => {
const text = item.querySelector('.panel-list-text').textContent.toLowerCase();
const indexStr = (parseInt(item.getAttribute('data-index'), 10) + 1).toString();
const match = keyword === '' || text.includes(keyword) || indexStr.includes(keyword);
item.style.display = match ? 'flex' : 'none';
});
});
thinkBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const editor = document.querySelector('#prompt-textarea');
if (!editor) return;
editor.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
editor.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
editor.dispatchEvent(new MouseEvent('click', { bubbles: true }));
editor.focus();
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
await new Promise(r => setTimeout(r, 50));
document.execCommand('insertText', false, '/think');
await new Promise(r => setTimeout(r, 150));
editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
await new Promise(r => setTimeout(r, 100));
editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
const originalColor = thinkBtn.style.color;
thinkBtn.style.color = '#10a37f';
setTimeout(() => thinkBtn.style.color = originalColor, 1000);
});
function updateIndicator(targetDot) {
if (!targetDot) { activeIndicator.style.opacity = '0'; return; }
activeIndicator.style.opacity = '1';
activeIndicator.style.transform = `translateY(${targetDot.offsetTop}px)`;
activeIndicator.style.boxShadow = targetDot.classList.contains('highlighted') ? '0 0 0 3px rgba(255, 193, 7, 0.5)' : '0 0 0 3px var(--tl-dot-active-ring)';
}
function jumpToTurn(index, turnNode, sourceDot) {
globalTooltip.classList.remove('visible');
isAutoScrolling = true;
turnNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
document.querySelectorAll('.timeline-dot').forEach(d => d.classList.remove('active'));
const targetDot = sourceDot || document.querySelector(`.timeline-dot[data-index="${index}"]`);
if (targetDot) { targetDot.classList.add('active'); updateIndicator(targetDot); }
document.querySelectorAll('.panel-list-item').forEach(item => item.classList.remove('active'));
const targetListItem = document.querySelector(`.panel-list-item[data-index="${index}"]`);
if (targetListItem) targetListItem.classList.add('active');
}
window.addEventListener('scroll', () => {
if (isAutoScrolling) {
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => { isAutoScrolling = false; }, 150);
}
}, { passive: true });
const scrollObserver = new IntersectionObserver((entries) => {
if (isAutoScrolling) return;
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = entry.target.getAttribute('data-timeline-index');
if (index === null) return;
document.querySelectorAll('.timeline-dot').forEach(dot => dot.classList.remove('active'));
const activeDot = document.querySelector(`.timeline-dot[data-index="${index}"]`);
if (activeDot) { activeDot.classList.add('active'); updateIndicator(activeDot); }
document.querySelectorAll('.panel-list-item').forEach(item => item.classList.remove('active'));
const targetListItem = document.querySelector(`.panel-list-item[data-index="${index}"]`);
if (targetListItem) targetListItem.classList.add('active');
}
});
}, { rootMargin: '-20% 0px -60% 0px', threshold: 0 });
// 统一公式点击嗅探交互
function enhanceFormulas() {
document.querySelectorAll('.katex-display:not(.enhanced-formula), .katex:not(.katex-display):not(.enhanced-formula)').forEach(mathNode => {
if (!mathNode.classList.contains('katex-display') && mathNode.closest('.katex-display')) {
return;
}
const annotation = mathNode.querySelector('annotation[encoding="application/x-tex"]');
if (!annotation) return;
mathNode.classList.add('enhanced-formula');
const isBlock = mathNode.classList.contains('katex-display');
const tex = annotation.textContent.trim();
mathNode.title = isBlock ? '点击复制行间公式' : '点击复制行内公式';
mathNode.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const textToCopy = isBlock ? `$$\n${tex}\n$$` : `$${tex}$`;
navigator.clipboard.writeText(textToCopy).then(() => {
formulaToast.style.left = `${e.clientX}px`;
formulaToast.style.top = `${e.clientY}px`;
formulaToast.classList.add('show');
if (formulaToast.timeoutId) clearTimeout(formulaToast.timeoutId);
formulaToast.timeoutId = setTimeout(() => {
formulaToast.classList.remove('show');
}, 1500);
});
});
});
}
// 提取并导出单条回答为Markdown
function exportTurnToMarkdown(actionContainer) {
const turnNode = actionContainer.closest('[data-testid^="conversation-turn-"], [data-turn="assistant"]');
if (!turnNode) return;
let mdContent = getAssistantTurnMarkdown(turnNode);
if (!mdContent || mdContent.trim() === '') return;
const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ChatGPT_Response_${new Date().toISOString().replace(/[:.]/g, '-')}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 挂载导出Markdown按钮
function addExportButtons() {
document.querySelectorAll('[data-testid="copy-turn-action-button"]').forEach(copyBtn => {
const container = copyBtn.parentElement;
if (!container || container.querySelector('.export-md-btn')) return;
const exportBtn = document.createElement('button');
exportBtn.className = 'export-md-btn text-token-text-secondary hover:bg-token-bg-secondary rounded-lg';
exportBtn.setAttribute('aria-label', '导出为Markdown');
exportBtn.setAttribute('title', '导出为Markdown');
exportBtn.innerHTML = ``;
exportBtn.addEventListener('click', (e) => {
e.stopPropagation();
exportTurnToMarkdown(container);
});
container.appendChild(exportBtn);
});
}
function updateTimeline() {
enhanceFormulas();
addExportButtons();
const userTurns = document.querySelectorAll('section[data-turn="user"]');
const existingDots = document.querySelectorAll('.timeline-dot');
const highlightedIndexes = new Set();
document.querySelectorAll('.timeline-dot.highlighted').forEach(d => highlightedIndexes.add(d.getAttribute('data-index')));
if (userTurns.length === 0) {
container.style.display = 'none';
menuBtn.style.display = 'none';
panel.classList.remove('visible');
return;
} else {
container.style.display = 'flex';
menuBtn.style.display = 'flex';
}
let needsRedraw = userTurns.length !== existingDots.length;
userTurns.forEach((turn, index) => {
const turnId = turn.getAttribute('data-turn-id') || `turn-${index}`;
const messageDiv = turn.querySelector('[data-message-author-role="user"]');
if (messageDiv) {
let rawText = messageDiv.innerText.replace(/\s+/g, ' ').trim();
let previewText = rawText.length > 120 ? rawText.substring(0, 120) + '...' : rawText;
if (previewText.length === 0) previewText = '[图片或文件]';
if (turnTextCache.get(turnId) !== previewText) {
turnTextCache.set(turnId, previewText);
if (!needsRedraw) {
const listItemText = panelList.querySelector(`.panel-list-item[data-index="${index}"] .panel-list-text`);
if (listItemText) {
listItemText.textContent = previewText;
}
}
}
} else if (!turnTextCache.has(turnId)) {
turnTextCache.set(turnId, '[已折叠的历史消息]');
}
const observeTarget = turn.closest('[data-turn-id-container]') || turn;
observeTarget.setAttribute('data-timeline-index', index);
scrollObserver.observe(observeTarget);
});
if (!needsRedraw) return;
container.querySelectorAll('.timeline-dot').forEach(d => d.remove());
scrollObserver.disconnect();
panelList.innerHTML = '';
userTurns.forEach((turn, index) => {
const turnId = turn.getAttribute('data-turn-id') || `turn-${index}`;
const previewText = turnTextCache.get(turnId);
const observeTarget = turn.closest('[data-turn-id-container]') || turn;
observeTarget.setAttribute('data-timeline-index', index);
scrollObserver.observe(observeTarget);
const dot = document.createElement('div');
dot.className = 'timeline-dot';
dot.setAttribute('data-index', index);
if (highlightedIndexes.has(index.toString())) dot.classList.add('highlighted');
const listItem = document.createElement('div');
listItem.className = 'panel-list-item';
listItem.setAttribute('data-index', index);
// 全新的序号徽章 + 文本布局
listItem.innerHTML = `${index + 1}
${previewText}
`;
if (highlightedIndexes.has(index.toString())) listItem.classList.add('highlighted');
if (index === userTurns.length - 1) {
dot.classList.add('active');
listItem.classList.add('active');
requestAnimationFrame(() => updateIndicator(dot));
}
dot.addEventListener('mouseenter', () => {
const rect = dot.getBoundingClientRect();
globalTooltip.innerText = turnTextCache.get(turnId);
globalTooltip.style.top = `${rect.top + rect.height / 2}px`;
globalTooltip.style.right = `${window.innerWidth - rect.left + 14}px`;
globalTooltip.classList.add('visible');
});
dot.addEventListener('mouseleave', () => globalTooltip.classList.remove('visible'));
let pressTimer;
let isLongPress = false;
dot.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isLongPress = false;
pressTimer = setTimeout(() => {
isLongPress = true;
dot.classList.toggle('highlighted');
listItem.classList.toggle('highlighted');
if (dot.classList.contains('active')) updateIndicator(dot);
}, 450);
});
dot.addEventListener('mouseup', () => clearTimeout(pressTimer));
dot.addEventListener('mouseleave', () => clearTimeout(pressTimer));
dot.addEventListener('click', (e) => { if (!isLongPress) jumpToTurn(index, turn, dot); });
listItem.addEventListener('click', () => jumpToTurn(index, turn, dot));
container.appendChild(dot);
panelList.appendChild(listItem);
});
}
let debounceTimer;
const domObserver = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(updateTimeline, 600);
});
domObserver.observe(document.body, { childList: true, subtree: true });
updateTimeline();
})();