ChatGPT GPT-4 Counters

Add counters (and reset time indicators) for GPT-4/Custom GPTs to ChatGPT

目前为 2023-12-18 提交的版本。查看 最新版本

// ==UserScript==
// @name         ChatGPT GPT-4 Counters
// @namespace 	 lugia19.com
// @version      1.0
// @description  Add counters (and reset time indicators) for GPT-4/Custom GPTs to ChatGPT
// @author       lugia19
// @license		 MIT
// @match        https://chat.openai.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// ==/UserScript==

//If you want to have the next reset time show up on all webpages, simply change the match to be https://*/*

if (window.top != window.self)
	return	//This is because chat.openai.com uses multiple threads. This way we avoid hooking multiple times, which causes issues.


let debug_logs = true

let gpt_4_cap = 40
let custom_gpts_cap = 25

let gpt_4_reset_time = 3 * 60 * 60000
let custom_gpts_reset_time = 3 * 60 * 60000
let timer_check_frequency = 60000


let gpt_4_key = "gpt_4"
let custom_gpts_key = "custom_gpts"

let default_timer_bg_color = "rgba(0, 0, 0, 0.5)"
let red_timer_bg_color = "rgba(180, 0, 0, 0.5)"
let default_padding = '10px'



let itemGrid = document.createElement('div');
let reset_counter_constant = -999	//Used to fix a bug where resetting the custom GPTs counter when it was at 1 would make the GPT-4 counter decrease.

Object.assign(itemGrid.style, {
	position: 'fixed',
	right: '70px',
	bottom: '0',
	zIndex: '1000',
	display: 'grid',
	gridTemplateColumns: 'repeat(2, 1fr)',
	gridTemplateRows: 'repeat(2, 1fr)',
	gap: '10px'
});

if (!window.location.toString().includes("chat.openai.com")) {
	itemGrid.style.gridTemplateRows = 'repeat(1, 1fr)'
}

document.body.append(itemGrid);



//Helper functions
function console_log(text) {
	if (debug_logs)
		console.log(text)
}

function getTwoDigits(value) {
	if (value < 10) {
		return `0${value}`
	} else {
		return `${value}`
	}
}

function formatTime(dateString) {
	if (dateString) {
		date = new Date(dateString)
		const hours = getTwoDigits(date.getHours());
		const mins = getTwoDigits(date.getMinutes());

		return `${hours}:${mins}`;
	} else {
		return ""
	}
}

function isThreeFive() {
	//Check if we're on 3.5...
	let radixItems = document.querySelectorAll('div[id^="radix-:"]');
	for (let item of radixItems) {
		for (let child of item.children) {
			if (child.matches('svg') && child.classList.contains("text-token-text-tertiary")) {
				console_log("Found the arrow. This is the model selector.")
				if (item.textContent == "ChatGPT 3.5") {
					console_log("Is 3.5, bail.")
					return true
				}
			}
		}
	}
	return false
}

//ResetTimer class
class ResetTimer {

	constructor(storageTimeKey, period) {
		this.timeKey = storageTimeKey;
		this.period = period
		this.counter = undefined

		// Create time input box
		this.timeInput = document.createElement('input');
		itemGrid.appendChild(this.timeInput);
		this.timeInput.type = 'time';

		// Specify Style
		Object.assign(this.timeInput.style, {
			backgroundColor: default_timer_bg_color,
			color: 'white',
			border: 'none',
			padding: default_padding,
			zIndex: '9999',
			textAlign: "center"
		});

		// Load the time
		let storedTimeString = GM_getValue(this.timeKey);
		this.timeInput.value = formatTime(storedTimeString);

		// Add Event Listener
		this.timeInput.addEventListener('change', () => this.handleTimeChange());

		// Add Value Change Listener
		GM_addValueChangeListener(this.timeKey, (key, oldVal, newVal, remote) => this.handleValueChange(key, oldVal, newVal, remote));

		setInterval(() => this.checkTime(), timer_check_frequency);
	}

	setBgColor(color) {
		this.timeInput.style.backgroundColor = color
	}

	handleTimeChange() {
		//Handle the user setting the value
		console_log("Timeinput change hit");
		console_log(this.timeInput.value);
		if (this.timeInput.value != "") {
			const [hours, minutes] = this.timeInput.value.split(':').map(Number);
			const currentTime = new Date();
			let newTime = new Date();

			newTime.setHours(hours, minutes, 0); // We don't care about seconds.

			// Check if the selected time has already passed for today
			if (newTime <= currentTime) {
				//User's chosen has passed, set `newTime` to tomorrow
				console_log("New time has already passed, changing date to tomorrow...")
				newTime.setDate(currentTime.getDate() + 1);
			}

			console_log("Updating time...");
			GM_setValue(this.timeKey, newTime.toISOString());
		} else {
			console_log("timeInput.value was empty, setting stored time to nothing")
			GM_setValue(this.timeKey, null);
		}
	}

	handleValueChange(key, oldValue, newValue, remote) {
		if (oldValue != newValue) {
			console_log("Updating time from storage...")
			console_log(formatTime(newValue))
			this.timeInput.value = formatTime(newValue)
		}
	}

	initializeTimeIfNull() {
		console_log(`initializeTimeIfNull called for ${this.timeKey}`)
		let resetTimeString = GM_getValue(this.timeKey)
		if (!resetTimeString) {
			console_log("Reset time not currently set, setting it...")
			let newResetTime = new Date((new Date()).getTime() + this.period);
			console_log(newResetTime)
			GM_setValue(this.timeKey, newResetTime.toISOString());
		}
	}

	checkTime() {
		const currentTime = new Date();
		console_log(`Time check by ${this.timeKey}...`)
		let storedResetTimeString = GM_getValue(this.timeKey)
		console_log(storedResetTimeString)
		if (!storedResetTimeString) {
			return	//No reset time set.
		}
		console_log("Reset time was set...")
		let resetTime = new Date(storedResetTimeString);
		if (resetTime) {
			if (currentTime >= resetTime) {
				//If this timer has a counter assigned, reset it
				if (this.counter) {
					console_log("Resetting counter...")
					this.counter.saveAndUpdate(reset_counter_constant)
				}

				console_log("Reset time triggered.")
				//Set stored reset time to null.
				GM_setValue(this.timeKey, null);
				this.timeInput.value = ""
			}
		}
	}
}

const gpt_4_timer = new ResetTimer(gpt_4_key + "_timer", gpt_4_reset_time)
const custom_gpts_timer = new ResetTimer(custom_gpts_key + "_timer", custom_gpts_reset_time)

//If we're _not_ on chat.openai.com, we exit here (I personally keep the timer present on every page, to remind me of the reset time - hence the check).
if (!window.location.toString().includes("chat.openai.com")) {
	return
}

class Counter {
	constructor(key, max_value, label, bg_color, top, callback) {
		this.key = key;
		this.max_value = max_value
		this.label = label;
		this.bg_color = bg_color;
		this.top = top;
		this.callback = callback;
		this.initElements();
		GM_addValueChangeListener(this.key, this.valueListener.bind(this));
	}

	//Listen for storage changes
	valueListener(key, oldValue, newValue, remote) {
		if (oldValue != newValue)
			this.saveAndUpdate(parseInt(newValue));
	}

	// Function to save counter to localStorage and update display
	saveAndUpdate(value) {
		//Check that the value is valid...
		console_log(`saveandupdate ${this.key} value check... ${value}`)
		if (value < 0 && value != reset_counter_constant)
			value -= reset_counter_constant

		// Call the callback if it exists
		console_log(`Calling callback for ${this.key}`)
		if (typeof this.callback === 'function') {
			this.callback(value);
		}
		console_log(`Done calling callback for ${this.key}`)
		GM_setValue(this.key, value);

		// Update the counter display (we show reset_counter_constant as 0, despite it being -999)
		if (value == reset_counter_constant)
			value = 0

		this.counterText.textContent = `${this.label}: ${value}/${this.max_value}`;
	}

	getValue() {
		let value = parseInt(GM_getValue(this.key));
		if (!value && value != 0) {
			value = reset_counter_constant
		}
		return value
	}

	createButtons() {
		this.buttonContainer = document.createElement('div');
		Object.assign(this.buttonContainer.style, {
			display: 'flex',
			justifyContent: 'center',
			gap: '5px',
			display: 'none'	//Hide buttons initially
		});

		['+', '-', 'Reset'].forEach(text => {
			const button = document.createElement('button');
			button.textContent = text;
			button.onclick = () => {
				let current_value = parseInt(GM_getValue(this.key));
				if (!current_value && current_value != 0) {
					current_value = reset_counter_constant
				}
				console_log(current_value)
				console_log(text)
				if (current_value == reset_counter_constant) {
					if (text === "+")
						current_value = 1
				} else {
					if (text === '+') current_value += 1;
					else if (text === '-') current_value -= 1;
					else current_value = reset_counter_constant;
				}

				console_log(current_value)
				if (current_value < 0 && current_value != reset_counter_constant) current_value = 0;
				this.saveAndUpdate(current_value);

			};
			button.style.margin = '0 5px';
			button.style.display = 'inline-block';
			this.buttonContainer.appendChild(button);
		});
		this.counterDisplay.appendChild(this.buttonContainer);
	}



	initElements() {
		// Create counter display
		this.counterDisplay = document.createElement('div');
		itemGrid.appendChild(this.counterDisplay);
		// Create the counter text
		this.counterText = document.createElement('p');
		this.counterText.style.textAlign = "center"
		this.counterDisplay.appendChild(this.counterText);

		// Customization
		this.counterDisplay.id = `${this.key}-display`;
		// Specify Style
		Object.assign(this.counterDisplay.style, {
			backgroundColor: this.bg_color,
			color: 'white',
			border: 'none',
			padding: default_padding,
			zIndex: '1000',
			display: 'grid', // Changed from 'flex' to 'grid'
			gridTemplateRows: 'auto 1fr', // The counterText will take up the space it needs, and the buttons will take the rest
			alignItems: 'center',
			justifyContent: "center",
			justifyItems: "center"
		});

		this.createButtons()

		// Load counter from localStorage or set to reset_counter_constant if not present
		let current_value = parseInt(GM_getValue(this.key));
		if (!current_value && current_value != 0) {
			current_value = reset_counter_constant
		}
		this.saveAndUpdate(current_value);
	}
}

// Create counters
let gpt_4_counter = new Counter(gpt_4_key + "_counter", gpt_4_cap, "GPT-4", 'rgba(119, 54, 135, 0.5)', '50px', undefined);
gpt_4_timer.counter = gpt_4_counter

let custom_gpts_counter = new Counter(custom_gpts_key + "_counter", custom_gpts_cap, "Custom GPTs", 'rgba(70, 130, 180, 0.5)', '100px', undefined);
custom_gpts_timer.counter = custom_gpts_counter

//Set the timer colors depending on the counter values
function setTimerColors() {
	let gpt_4_value = gpt_4_counter.getValue()
	let custom_gpt_value = custom_gpts_counter.getValue()
	let gpt_4_max = gpt_4_counter.max_value
	let custom_gpt_max = custom_gpts_counter.max_value

	if (gpt_4_value >= gpt_4_max) {
		gpt_4_timer.setBgColor(red_timer_bg_color)
		custom_gpts_timer.setBgColor(red_timer_bg_color)
	} else if (custom_gpt_value >= custom_gpt_max) {
		gpt_4_timer.setBgColor(default_timer_bg_color)
		custom_gpts_timer.setBgColor(red_timer_bg_color)
	} else {
		gpt_4_timer.setBgColor(default_timer_bg_color)
		custom_gpts_timer.setBgColor(default_timer_bg_color)
	}
}

// Function to be called in saveAndUpdate for the GPT-4 counter
const gpt4CounterCallback = (value) => {
	if (value > 0)
		gpt_4_timer.initializeTimeIfNull()
	setTimerColors()
};

// Function to be called in saveAndUpdate for Counter 2
const customGPTSCounterCallback = (value) => {
	if (value > 0)
		custom_gpts_timer.initializeTimeIfNull()

	let old_value = parseInt(GM_getValue(custom_gpts_key + "_counter"));
	if (!old_value && old_value != 0) {
		old_value = reset_counter_constant
	}

	//This is all pretty convoluted, I know.
	console_log(`value: ${value}`)
	console_log(`old_value: ${old_value}`)
	if (value != reset_counter_constant) {
		let diff = value - old_value
		console_log(`diff: ${diff}`)
		if (-1 <= diff && diff <= 1 && diff != 0 && value != reset_counter_constant) {
			console_log("Updating counter1 from counter2 change...");
			let new_value = gpt_4_counter.getValue() + diff
			if (new_value < 0)
				new_value = 0
			console_log(`New value: ${new_value}`)
			// Use global counter1 object for calling the instance method
			gpt_4_counter.saveAndUpdate(new_value);
		} else if (old_value = reset_counter_constant && value > 0 && diff != 0) {
			console_log("Updating gpt-4 counter due to custom coming out of reset...")
			if (gpt_4_counter.getValue() != reset_counter_constant) {
				gpt_4_counter.saveAndUpdate(gpt_4_counter.getValue() + 1)
			} else {
				gpt_4_counter.saveAndUpdate(1)
			}

		}
	}


	setTimerColors()
};

gpt_4_counter.callback = gpt4CounterCallback
custom_gpts_counter.callback = customGPTSCounterCallback




//Set callbacks to show/hide counter buttons
itemGrid.addEventListener('mouseenter', () => {
	gpt_4_counter.buttonContainer.style.display = ""
	custom_gpts_counter.buttonContainer.style.display = ""
});

itemGrid.addEventListener('mouseleave', () => {
	gpt_4_counter.buttonContainer.style.display = "none"
	custom_gpts_counter.buttonContainer.style.display = "none"
});

//Automatically update counters via event delegation/bubbling
console_log("Adding event listeners...")

//keyup event (for sending messages by hitting enter)
function handleKeyup(event) {
	// Check if the event's target is the #prompt-textarea
	if (event.target.matches('#prompt-textarea') && event.key === 'Enter' && !event.shiftKey) {
		console_log("Enter pressed in textarea, without shift.");
		if (isThreeFive())
			return
		console_log("Increasing counter.")
		let is_custom = window.location.toString().includes("https://chat.openai.com/g/"); // Is custom GPT?
		let counter = is_custom ? custom_gpts_counter : gpt_4_counter;
		counter.saveAndUpdate(counter.getValue() + 1);
	}
}

//Helper functions to traverse DOM...
function get_parent_message(element) {
	console_log(element)
	let parent_message = element
	while (parent_message) {
		console_log("Searching for parent message...")
		if (parent_message.classList.contains("text-token-text-primary")) {
			console_log("Found parent message, returning.")
			return parent_message
		}
		parent_message = parent_message.parentElement
		console_log(parent_message)
	}
	return undefined
}

function is_message_assistant(message) {
	let isAssistant = undefined
	if (!message)
		return undefined

	//Look for the first element with multiple children.
	while (message.childElementCount == 1) {
		message = message.lastChild
	}
	console_log("Iterating over children...")
	for (let child of message.children) {
		console_log(child)
		if (child.classList.contains("w-full")) {
			isAssistant = child.classList.contains("agent-turn")
			break
		}
	}
	return isAssistant
}

//Clicker event, for all the buttons.
function handleClick(event) {
	console_log(event.target)

	if (isThreeFive())	//We just exit immediately.
		return

	//Get first button in tree.
	let target_btn = event.target
	while (target_btn && !target_btn.matches("button"))
		target_btn = target_btn.parentElement

	console_log(target_btn)

	let should_increase = false

	if (target_btn) {	//Only continue if we found a button parent of event.target
		//Refresh button
		if (target_btn.matches('button.p-1.pl-0.rounded-md')) {
			let parent_msg = get_parent_message(target_btn)
			let isAssistant = is_message_assistant(parent_msg)
			console_log(isAssistant)

			if (isAssistant) {
				console_log("Is assistant button.")
				if (target_btn == target_btn.parentElement.lastChild) {
					console_log("Is last button.")
					if (target_btn.parentElement.classList.contains("text-gray-400")) {
						should_increase = true
					} else {
						console_log("Is dislike - ignore.")
					}
				} else {
					console_log("Is not last button - ignore.")
				}
			} else {
				console_log("Is not assistant button - ignore.")
			}
		}

		//Send button
		if (target_btn.getAttribute("data-testid") == "send-button") {
			console_log("Is send button.")
			should_increase = true
		}

		//Save & Submit button
		if (target_btn.textContent && (target_btn.textContent == "Save & Submit" || target_btn.textContent == "Regenerate")) {
			console_log("Is save & submit or regenerate.")
			should_increase = true
		}

		if (target_btn.classList.contains("text-left") && target_btn.classList.contains("rounded-xl")) {
			console_log("Is example chat")
			should_increase = true
		}
	}

	if (should_increase) {
		console_log("Increasing counter...")
		let is_custom = window.location.toString().includes("https://chat.openai.com/g/")
		let counter = is_custom ? custom_gpts_counter : gpt_4_counter;
		counter.saveAndUpdate(counter.getValue() + 1)
	}
}

//Add event listeners to document
document.addEventListener('keyup', handleKeyup);
document.addEventListener('click', handleClick);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址