Size
15.7 KB
Version
1.0.1
Created
Oct 26, 2025
Updated
4 months ago
1// ==UserScript==
2// @name ChatGPT 聊天时间线筛选器
3// @description 为ChatGPT聊天添加时间线筛选和快速定位功能,支持筛选用户消息和GPT回复
4// @version 1.0.1
5// @match https://*.chatgpt.com/*
6// @icon https://cdn.oaistatic.com/assets/favicon-l4nq08hd.svg
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('ChatGPT 聊天时间线筛选器已启动');
12
13 // 防抖函数
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // 创建时间线面板
27 function createTimelinePanel() {
28 const panel = document.createElement('div');
29 panel.id = 'chatgpt-timeline-panel';
30 panel.innerHTML = `
31 <div id="timeline-header">
32 <h3>聊天时间线</h3>
33 <button id="timeline-close-btn" title="关闭">×</button>
34 </div>
35 <div id="timeline-filters">
36 <button class="filter-btn active" data-filter="all">全部</button>
37 <button class="filter-btn" data-filter="user">用户消息</button>
38 <button class="filter-btn" data-filter="assistant">GPT回复</button>
39 </div>
40 <div id="timeline-list"></div>
41 `;
42 document.body.appendChild(panel);
43
44 // 添加样式
45 addStyles();
46
47 // 绑定事件
48 bindEvents();
49 }
50
51 // 添加样式
52 function addStyles() {
53 const style = document.createElement('style');
54 style.textContent = `
55 #chatgpt-timeline-panel {
56 position: fixed;
57 top: 80px;
58 right: 20px;
59 width: 320px;
60 max-height: 70vh;
61 background: #ffffff;
62 border: 1px solid #d1d5db;
63 border-radius: 12px;
64 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
65 z-index: 10000;
66 display: flex;
67 flex-direction: column;
68 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
69 }
70
71 .dark #chatgpt-timeline-panel {
72 background: #2d2d2d;
73 border-color: #4a4a4a;
74 color: #e5e5e5;
75 }
76
77 #timeline-header {
78 display: flex;
79 justify-content: space-between;
80 align-items: center;
81 padding: 16px;
82 border-bottom: 1px solid #e5e7eb;
83 }
84
85 .dark #timeline-header {
86 border-bottom-color: #4a4a4a;
87 }
88
89 #timeline-header h3 {
90 margin: 0;
91 font-size: 16px;
92 font-weight: 600;
93 color: #111827;
94 }
95
96 .dark #timeline-header h3 {
97 color: #e5e5e5;
98 }
99
100 #timeline-close-btn {
101 background: none;
102 border: none;
103 font-size: 24px;
104 cursor: pointer;
105 color: #6b7280;
106 padding: 0;
107 width: 28px;
108 height: 28px;
109 display: flex;
110 align-items: center;
111 justify-content: center;
112 border-radius: 6px;
113 transition: all 0.2s;
114 }
115
116 #timeline-close-btn:hover {
117 background: #f3f4f6;
118 color: #111827;
119 }
120
121 .dark #timeline-close-btn:hover {
122 background: #404040;
123 color: #e5e5e5;
124 }
125
126 #timeline-filters {
127 display: flex;
128 gap: 8px;
129 padding: 12px 16px;
130 border-bottom: 1px solid #e5e7eb;
131 }
132
133 .dark #timeline-filters {
134 border-bottom-color: #4a4a4a;
135 }
136
137 .filter-btn {
138 flex: 1;
139 padding: 8px 12px;
140 border: 1px solid #d1d5db;
141 background: #ffffff;
142 border-radius: 8px;
143 cursor: pointer;
144 font-size: 13px;
145 font-weight: 500;
146 color: #374151;
147 transition: all 0.2s;
148 }
149
150 .dark .filter-btn {
151 background: #3a3a3a;
152 border-color: #4a4a4a;
153 color: #d1d5db;
154 }
155
156 .filter-btn:hover {
157 background: #f9fafb;
158 border-color: #9ca3af;
159 }
160
161 .dark .filter-btn:hover {
162 background: #454545;
163 border-color: #5a5a5a;
164 }
165
166 .filter-btn.active {
167 background: #10a37f;
168 border-color: #10a37f;
169 color: #ffffff;
170 }
171
172 .dark .filter-btn.active {
173 background: #10a37f;
174 border-color: #10a37f;
175 }
176
177 #timeline-list {
178 overflow-y: auto;
179 padding: 12px;
180 flex: 1;
181 }
182
183 #timeline-list::-webkit-scrollbar {
184 width: 6px;
185 }
186
187 #timeline-list::-webkit-scrollbar-track {
188 background: transparent;
189 }
190
191 #timeline-list::-webkit-scrollbar-thumb {
192 background: #d1d5db;
193 border-radius: 3px;
194 }
195
196 .dark #timeline-list::-webkit-scrollbar-thumb {
197 background: #4a4a4a;
198 }
199
200 .timeline-item {
201 padding: 12px;
202 margin-bottom: 8px;
203 border-radius: 8px;
204 cursor: pointer;
205 transition: all 0.2s;
206 border: 1px solid #e5e7eb;
207 background: #f9fafb;
208 }
209
210 .dark .timeline-item {
211 background: #3a3a3a;
212 border-color: #4a4a4a;
213 }
214
215 .timeline-item:hover {
216 background: #f3f4f6;
217 border-color: #10a37f;
218 transform: translateX(-2px);
219 }
220
221 .dark .timeline-item:hover {
222 background: #454545;
223 border-color: #10a37f;
224 }
225
226 .timeline-item-header {
227 display: flex;
228 align-items: center;
229 gap: 8px;
230 margin-bottom: 6px;
231 }
232
233 .timeline-item-badge {
234 padding: 2px 8px;
235 border-radius: 4px;
236 font-size: 11px;
237 font-weight: 600;
238 text-transform: uppercase;
239 }
240
241 .timeline-item-badge.user {
242 background: #dbeafe;
243 color: #1e40af;
244 }
245
246 .dark .timeline-item-badge.user {
247 background: #1e3a8a;
248 color: #93c5fd;
249 }
250
251 .timeline-item-badge.assistant {
252 background: #d1fae5;
253 color: #065f46;
254 }
255
256 .dark .timeline-item-badge.assistant {
257 background: #064e3b;
258 color: #6ee7b7;
259 }
260
261 .timeline-item-index {
262 font-size: 11px;
263 color: #9ca3af;
264 font-weight: 500;
265 }
266
267 .timeline-item-content {
268 font-size: 13px;
269 color: #4b5563;
270 line-height: 1.5;
271 overflow: hidden;
272 text-overflow: ellipsis;
273 display: -webkit-box;
274 -webkit-line-clamp: 2;
275 -webkit-box-orient: vertical;
276 }
277
278 .dark .timeline-item-content {
279 color: #d1d5db;
280 }
281
282 #timeline-toggle-btn {
283 position: fixed;
284 top: 80px;
285 right: 20px;
286 width: 48px;
287 height: 48px;
288 background: #10a37f;
289 border: none;
290 border-radius: 50%;
291 cursor: pointer;
292 box-shadow: 0 4px 12px rgba(16, 163, 127, 0.3);
293 z-index: 9999;
294 display: flex;
295 align-items: center;
296 justify-content: center;
297 transition: all 0.3s;
298 color: #ffffff;
299 font-size: 20px;
300 font-weight: bold;
301 }
302
303 #timeline-toggle-btn:hover {
304 background: #0d8c6d;
305 transform: scale(1.05);
306 box-shadow: 0 6px 16px rgba(16, 163, 127, 0.4);
307 }
308
309 .timeline-empty {
310 text-align: center;
311 padding: 40px 20px;
312 color: #9ca3af;
313 font-size: 14px;
314 }
315
316 .dark .timeline-empty {
317 color: #6b7280;
318 }
319 `;
320 document.head.appendChild(style);
321 }
322
323 // 创建切换按钮
324 function createToggleButton() {
325 const btn = document.createElement('button');
326 btn.id = 'timeline-toggle-btn';
327 btn.innerHTML = '📋';
328 btn.title = '打开聊天时间线';
329 document.body.appendChild(btn);
330
331 btn.addEventListener('click', () => {
332 const panel = document.getElementById('chatgpt-timeline-panel');
333 if (panel) {
334 panel.remove();
335 btn.innerHTML = '📋';
336 btn.title = '打开聊天时间线';
337 } else {
338 createTimelinePanel();
339 updateTimeline();
340 btn.innerHTML = '✕';
341 btn.title = '关闭聊天时间线';
342 }
343 });
344 }
345
346 // 绑定事件
347 function bindEvents() {
348 // 关闭按钮
349 document.getElementById('timeline-close-btn').addEventListener('click', () => {
350 document.getElementById('chatgpt-timeline-panel').remove();
351 const toggleBtn = document.getElementById('timeline-toggle-btn');
352 if (toggleBtn) {
353 toggleBtn.innerHTML = '📋';
354 toggleBtn.title = '打开聊天时间线';
355 }
356 });
357
358 // 筛选按钮
359 document.querySelectorAll('.filter-btn').forEach(btn => {
360 btn.addEventListener('click', (e) => {
361 document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
362 e.target.classList.add('active');
363 updateTimeline(e.target.dataset.filter);
364 });
365 });
366 }
367
368 // 获取所有消息
369 function getAllMessages() {
370 const messages = [];
371 const articles = document.querySelectorAll('article[data-testid^="conversation-turn-"]');
372
373 console.log(`找到 ${articles.length} 条消息`);
374
375 articles.forEach((article, index) => {
376 const role = article.getAttribute('data-turn');
377 let content = '';
378
379 if (role === 'user') {
380 const contentDiv = article.querySelector('[data-message-author-role="user"] .whitespace-pre-wrap');
381 content = contentDiv ? contentDiv.textContent.trim() : '';
382 } else if (role === 'assistant') {
383 const contentDiv = article.querySelector('[data-message-author-role="assistant"] .markdown');
384 content = contentDiv ? contentDiv.textContent.trim() : '';
385 }
386
387 if (content) {
388 messages.push({
389 index: index + 1,
390 role: role,
391 content: content,
392 element: article
393 });
394 }
395 });
396
397 return messages;
398 }
399
400 // 更新时间线
401 function updateTimeline(filter = 'all') {
402 const listContainer = document.getElementById('timeline-list');
403 if (!listContainer) return;
404
405 const messages = getAllMessages();
406 const filteredMessages = filter === 'all'
407 ? messages
408 : messages.filter(msg => msg.role === filter);
409
410 console.log(`筛选后显示 ${filteredMessages.length} 条消息 (筛选条件: ${filter})`);
411
412 if (filteredMessages.length === 0) {
413 listContainer.innerHTML = '<div class="timeline-empty">暂无消息</div>';
414 return;
415 }
416
417 listContainer.innerHTML = filteredMessages.map(msg => {
418 const preview = msg.content.length > 100
419 ? msg.content.substring(0, 100) + '...'
420 : msg.content;
421
422 return `
423 <div class="timeline-item" data-index="${msg.index}">
424 <div class="timeline-item-header">
425 <span class="timeline-item-badge ${msg.role}">
426 ${msg.role === 'user' ? '用户' : 'GPT'}
427 </span>
428 <span class="timeline-item-index">#${msg.index}</span>
429 </div>
430 <div class="timeline-item-content">${preview}</div>
431 </div>
432 `;
433 }).join('');
434
435 // 绑定点击事件
436 listContainer.querySelectorAll('.timeline-item').forEach(item => {
437 item.addEventListener('click', () => {
438 const index = parseInt(item.dataset.index);
439 scrollToMessage(index);
440 });
441 });
442 }
443
444 // 滚动到指定消息
445 function scrollToMessage(index) {
446 const messages = getAllMessages();
447 const targetMessage = messages.find(msg => msg.index === index);
448
449 if (targetMessage && targetMessage.element) {
450 console.log(`滚动到消息 #${index}`);
451 targetMessage.element.scrollIntoView({
452 behavior: 'smooth',
453 block: 'center'
454 });
455
456 // 高亮效果
457 targetMessage.element.style.transition = 'background-color 0.3s';
458 const originalBg = window.getComputedStyle(targetMessage.element).backgroundColor;
459 targetMessage.element.style.backgroundColor = 'rgba(16, 163, 127, 0.1)';
460
461 setTimeout(() => {
462 targetMessage.element.style.backgroundColor = originalBg;
463 setTimeout(() => {
464 targetMessage.element.style.transition = '';
465 }, 300);
466 }, 1000);
467 }
468 }
469
470 // 监听DOM变化
471 function observeMessages() {
472 const observer = new MutationObserver(debounce(() => {
473 const panel = document.getElementById('chatgpt-timeline-panel');
474 if (panel) {
475 const activeFilter = document.querySelector('.filter-btn.active');
476 const filter = activeFilter ? activeFilter.dataset.filter : 'all';
477 updateTimeline(filter);
478 console.log('检测到消息变化,已更新时间线');
479 }
480 }, 1000));
481
482 // 观察整个文档的变化
483 observer.observe(document.body, {
484 childList: true,
485 subtree: true
486 });
487
488 console.log('已启动消息监听');
489 }
490
491 // 初始化
492 function init() {
493 console.log('初始化 ChatGPT 聊天时间线筛选器');
494
495 // 等待页面加载完成
496 if (document.readyState === 'loading') {
497 document.addEventListener('DOMContentLoaded', () => {
498 setTimeout(() => {
499 createToggleButton();
500 observeMessages();
501 }, 1000);
502 });
503 } else {
504 setTimeout(() => {
505 createToggleButton();
506 observeMessages();
507 }, 1000);
508 }
509 }
510
511 init();
512})();