Reveal hidden timestamps in Claude conversations with robust DOM/API handling, XHR support, and debounced observer; always show year; larger font.
当前为
// ==UserScript==
// @name Displays Date & Times within Anthropic Claude Conversations (ES5-safe)
// @namespace http://tampermonkey.net/
// @version 2.6
// @license MIT
// @description Reveal hidden timestamps in Claude conversations with robust DOM/API handling, XHR support, and debounced observer; always show year; larger font.
// @author Wayne
// @match https://claude.ai/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
var CONFIG = {
timestampClass: 'claude-timestamp',
messageSelector: "[data-testid*='message'], [class*='message'], .font-claude-message, [role='article'], div[class*='group']",
observerDelay: 500,
debounceDelay: 250,
timestampFormat: 'absolute',
position: 'inline',
maxJsonScriptSize: 500000, // Increased limit
timestampFontSizePx: 15,
debugMode: true // Enable debugging
};
function log(message, data) {
if (CONFIG.debugMode) {
console.log('[Claude Timestamps]', message, data || '');
}
}
function formatTimestamp(isoString) {
var date = new Date(isoString);
if (isNaN(date.getTime())) {
log('Invalid timestamp:', isoString);
return null;
}
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
year: 'numeric'
});
}
function injectStyles() {
if (document.getElementById('claude-timestamp-styles')) return;
var style = document.createElement('style');
style.id = 'claude-timestamp-styles';
style.textContent =
'.' + CONFIG.timestampClass + ' {' +
'font-size: ' + CONFIG.timestampFontSizePx + 'px;' +
'color: #5d6269;' +
'margin-bottom: 6px;' +
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;" +
'display: block;' +
'opacity: 0.7;' +
'}' +
'@media (prefers-color-scheme: dark) {' +
'.' + CONFIG.timestampClass + ' { color: #a2acba; }' +
'}';
document.head.appendChild(style);
log('Styles injected');
}
function debugMessageElement(element) {
if (!CONFIG.debugMode) return;
log('Debugging message element:', element);
log('Element classes:', element.className);
log('Element attributes:', Array.from(element.attributes || []).map(function(a) {
return a.name + '=' + a.value;
}));
// Check for React properties
var reactKeys = Object.keys(element).filter(function(k) {
return k.includes('react') || k.includes('React') || k.includes('fiber') || k.includes('Fiber');
});
log('React-related keys:', reactKeys);
// Check parent elements for data
var parent = element.parentElement;
if (parent) {
log('Parent classes:', parent.className);
log('Parent attributes:', Array.from(parent.attributes || []).map(function(a) {
return a.name + '=' + a.value;
}));
}
}
function extractTimestampFromAllScripts() {
var allScripts = document.querySelectorAll('script[type="application/json"], script:not([src])');
log('Found scripts:', allScripts.length);
for (var i = 0; i < allScripts.length; i++) {
var script = allScripts[i];
var content = script.textContent || script.innerHTML || '';
if (content.length > CONFIG.maxJsonScriptSize) {
log('Skipping large script:', content.length + ' chars');
continue;
}
if (content && (content.indexOf('created_at') > -1 || content.indexOf('timestamp') > -1 || content.indexOf('message') > -1)) {
try {
var data = JSON.parse(content);
log('Parsed script data keys:', Object.keys(data));
// Look for timestamps in various structures
if (data.created_at) return data.created_at;
if (data.timestamp) return data.timestamp;
if (data.messages && Array.isArray(data.messages) && data.messages.length > 0) {
var msg = data.messages[0];
if (msg.created_at) return msg.created_at;
if (msg.timestamp) return msg.timestamp;
}
if (data.conversation && data.conversation.created_at) return data.conversation.created_at;
} catch (e) {
// Not JSON or malformed
}
}
}
return null;
}
function extractTimestamp(messageElement) {
debugMessageElement(messageElement);
// Method 1: Direct data attributes
var dataAttrs = ['data-timestamp', 'data-created-at', 'data-time', 'data-message-time'];
for (var i = 0; i < dataAttrs.length; i++) {
var value = messageElement.getAttribute(dataAttrs[i]);
if (value) {
log('Found timestamp in data attribute:', value);
return value;
}
}
// Method 2: Check parent elements for data attributes
var current = messageElement;
for (var depth = 0; depth < 5 && current; depth++) {
for (var j = 0; j < dataAttrs.length; j++) {
var value = current.getAttribute(dataAttrs[j]);
if (value) {
log('Found timestamp in parent data attribute:', value);
return value;
}
}
current = current.parentElement;
}
// Method 3: Look for time elements
var timeElement = messageElement.querySelector('time[datetime]') ||
messageElement.parentElement?.querySelector('time[datetime]');
if (timeElement) {
var datetime = timeElement.getAttribute('datetime');
if (datetime) {
log('Found timestamp in time element:', datetime);
return datetime;
}
}
// Method 4: Search all JSON scripts
var scriptTimestamp = extractTimestampFromAllScripts();
if (scriptTimestamp) {
log('Found timestamp in script:', scriptTimestamp);
return scriptTimestamp;
}
// Method 5: Network intercepted data
if (window.conversationData && window.conversationData.messages) {
var messageId = messageElement.id || messageElement.getAttribute('data-message-id') ||
messageElement.getAttribute('data-testid');
for (var m = 0; m < window.conversationData.messages.length; m++) {
var msg = window.conversationData.messages[m];
if (msg.id === messageId || (messageId && msg.id && msg.id.indexOf(messageId) > -1)) {
if (msg.created_at) {
log('Found timestamp in intercepted data:', msg.created_at);
return msg.created_at;
}
}
}
// Fallback: use first message timestamp for all messages
if (window.conversationData.messages[0] && window.conversationData.messages[0].created_at) {
log('Using fallback timestamp from first message');
return window.conversationData.messages[0].created_at;
}
}
// Method 6: React Fiber (updated for modern React)
var keys = Object.keys(messageElement);
for (var k = 0; k < keys.length; k++) {
var key = keys[k];
if (key.indexOf('__reactFiber') === 0 || key.indexOf('__reactInternalInstance') === 0) {
try {
var fiber = messageElement[key];
// Navigate through fiber tree to find props
var current = fiber;
for (var attempts = 0; attempts < 10 && current; attempts++) {
if (current.memoizedProps) {
var props = current.memoizedProps;
if (props.message && props.message.created_at) {
log('Found timestamp in React fiber:', props.message.created_at);
return props.message.created_at;
}
if (props.created_at) {
log('Found timestamp in React props:', props.created_at);
return props.created_at;
}
if (props.timestamp) {
log('Found timestamp in React props:', props.timestamp);
return props.timestamp;
}
}
current = current.return || current.parent;
}
} catch (e) {
log('Error accessing React fiber:', e.message);
}
break;
}
}
log('No timestamp found for element');
return null;
}
function addTimestamp(messageElement, timestamp) {
if (messageElement.querySelector('.' + CONFIG.timestampClass)) return;
var formatted = formatTimestamp(timestamp);
if (!formatted) return;
var timestampElement = document.createElement('div');
timestampElement.className = CONFIG.timestampClass;
timestampElement.textContent = formatted;
// Insert at the beginning of the message
if (messageElement.firstChild) {
messageElement.insertBefore(timestampElement, messageElement.firstChild);
} else {
messageElement.appendChild(timestampElement);
}
log('Added timestamp to message:', formatted);
}
function processMessages() {
log('Processing messages...');
var messages = document.querySelectorAll(CONFIG.messageSelector);
log('Found message elements:', messages.length);
var timestampsAdded = 0;
for (var i = 0; i < messages.length; i++) {
var messageElement = messages[i];
// Skip if already has timestamp
if (messageElement.querySelector('.' + CONFIG.timestampClass)) continue;
var timestamp = extractTimestamp(messageElement);
if (timestamp) {
addTimestamp(messageElement, timestamp);
timestampsAdded++;
}
}
log('Timestamps added:', timestampsAdded);
}
function interceptNetworkData() {
if (!window.fetch) return;
var originalFetch = window.fetch;
window.fetch = function () {
var args = arguments;
var req = args[0];
var url = typeof req === 'string' ? req : (req && req.url ? req.url : '');
return originalFetch.apply(this, args).then(function (res) {
if (url && (url.indexOf('/conversations') > -1 || url.indexOf('/chat') > -1 || url.indexOf('/api') > -1)) {
log('Intercepted fetch to:', url);
try {
var clone = res.clone();
var ct = '';
try {
ct = clone.headers.get('content-type') || '';
} catch (e) {}
if (ct.indexOf('application/json') > -1) {
clone.json().then(function (data) {
if (data && (data.messages || data.conversation || data.chat)) {
log('Stored conversation data from fetch');
window.conversationData = data;
setTimeout(processMessages, 200);
}
})["catch"](function (e) {
log('Error parsing JSON from fetch:', e.message);
});
}
} catch (e) {
log('Error processing fetch response:', e.message);
}
}
return res;
});
};
}
function interceptXHR() {
var originalOpen = XMLHttpRequest.prototype.open;
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._interceptedUrl = url;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
var xhr = this;
var originalOnLoad = xhr.onload;
xhr.addEventListener('load', function () {
if (xhr._interceptedUrl &&
(xhr._interceptedUrl.indexOf('/conversations') > -1 ||
xhr._interceptedUrl.indexOf('/chat') > -1 ||
xhr._interceptedUrl.indexOf('/api') > -1)) {
log('Intercepted XHR to:', xhr._interceptedUrl);
try {
var ct = xhr.getResponseHeader('content-type') || '';
if (ct.indexOf('application/json') > -1) {
var data = JSON.parse(xhr.responseText);
if (data && (data.messages || data.conversation || data.chat)) {
log('Stored conversation data from XHR');
window.conversationData = data;
setTimeout(processMessages, 200);
}
}
} catch (e) {
log('Error processing XHR response:', e.message);
}
}
if (originalOnLoad) originalOnLoad.call(xhr);
});
return originalSend.apply(this, arguments);
};
}
function debounce(fn, delay) {
var timer;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
function setupObserver() {
var debouncedProcess = debounce(processMessages, CONFIG.debounceDelay);
var observer = new MutationObserver(function (mutations) {
var shouldProcess = false;
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (var n = 0; n < mutation.addedNodes.length; n++) {
var node = mutation.addedNodes[n];
if (node.nodeType === 1) {
// Check if node is a message or contains messages
if ((node.matches && node.matches(CONFIG.messageSelector)) ||
(node.querySelector && node.querySelector(CONFIG.messageSelector))) {
shouldProcess = true;
break;
}
}
}
}
if (shouldProcess) break;
}
if (shouldProcess) {
log('DOM mutation detected, processing messages');
debouncedProcess();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false, // Don't watch attributes to reduce noise
characterData: false
});
log('DOM observer setup complete');
return observer;
}
function init() {
log('Initializing Claude timestamp script');
injectStyles();
interceptNetworkData();
interceptXHR();
// Initial processing with increasing delays
setTimeout(processMessages, 1000);
setTimeout(processMessages, 3000);
setTimeout(processMessages, 5000);
setupObserver();
// Periodic processing for missed updates
setInterval(processMessages, 15000);
log('Initialization complete');
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 100);
}
})();