ChatGPT 聊天时间线筛选器

为ChatGPT聊天添加时间线筛选和快速定位功能,支持筛选用户消息和GPT回复

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})();
ChatGPT 聊天时间线筛选器 | Robomonkey