Sora Moderation Helper

Monitor image generation status from Sora, Auto-Retry, and filter only for successes

// ==UserScript==
// @name         Sora Moderation Helper
// @namespace    https://lugia19.com
// @version      0.12.1
// @description  Monitor image generation status from Sora, Auto-Retry, and filter only for successes
// @author       lugia19
// @license      MIT
// @match        https://sora.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @inject-into  page
// ==/UserScript==

(function () {
	'use strict';


	let lastUrl = null;
	let currentUIRetryCount = 1;
	let mainUI = null;
	let retryTracker = null;
	let retrySubmitter = null;

	let pollingInterval = null;
	let authToken = null;
	let lastInfo = null; // Track the last known status
	const MAX_AUTO_RETRIES = 30;


	/**
	 * Base class for managing the status card UI
	 */
	class MainUI {
		constructor() {
			this.card = null;
			this.contentDiv = null;
			this.init();
		}

		init() {
			if (this.card) return this.card;

			this.card = document.createElement('div');
			this.card.id = 'generation-status-card';
			this.card.style.cssText = `
            position: fixed;
            top: 80vh;
            right: 20px;
            width: 300px;
            padding: 15px;
            background-color: black;
            color: white;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            border: 2px solid #aaa;
            border-radius: 6px;
            z-index: 99999;
            font-family: Arial, sans-serif;
            display: none;
        `;

			const header = document.createElement('div');
			header.style.cssText = `
            margin-bottom: 10px;
            font-weight: bold;
            padding-bottom: 5px;
            border-bottom: 1px solid #444;
        `;
			header.textContent = 'Generation Status';

			this.contentDiv = document.createElement('div');
			this.contentDiv.id = 'generation-status-content';

			this.card.appendChild(header);
			this.card.appendChild(this.contentDiv);

			// Find container and append card
			let container = this.findContainer();
			container.appendChild(this.card);

			return this.card;
		}

		findContainer() {
			// Find a suitable container
			let container = document.querySelector('div[role="dialog"][data-state="open"]');
			if (!container) {
				container = document.querySelector('#__next') ||
					document.querySelector('.root') ||
					document.body;
			}
			return container;
		}

		updateStatus(taskInfo) {
			const { displayStatus, borderColor } = this.determineStatusAndColor(taskInfo);
			this.card.style.borderColor = borderColor;

			// Clear existing task status elements
			this.clearUI();

			// Add task info elements
			this.addTaskInfoElements(taskInfo, displayStatus);

			// Show the card
			this.show();

			return { displayStatus, borderColor };
		}

		clearUI() {
			// Keep UI components with specific IDs
			const children = Array.from(this.contentDiv.children);
			for (const child of children) {
				this.contentDiv.removeChild(child);
			}
		}

		addTaskInfoElements(taskInfo, displayStatus) {
			const taskIdDiv = document.createElement('div');
			taskIdDiv.innerHTML = `<strong>Task ID:</strong> ${taskInfo.id.replace('task_', '')}`;
			this.contentDiv.insertBefore(taskIdDiv, this.contentDiv.firstChild);

			const statusDiv = document.createElement('div');
			statusDiv.innerHTML = `<strong>Status:</strong> ${displayStatus}`;
			this.contentDiv.insertBefore(statusDiv, this.contentDiv.childNodes[1] || null);

			const progressDiv = document.createElement('div');
			progressDiv.innerHTML = `<strong>Progress:</strong> ${taskInfo.progress_pct !== undefined ? Math.round(taskInfo.progress_pct * 100) : 'undefined'}%`;
			this.contentDiv.insertBefore(progressDiv, this.contentDiv.childNodes[2] || null);
		}

		determineStatusAndColor(taskInfo) {
			let displayStatus = "N/A";
			let borderColor = "#aaa";

			if (taskInfo.status === "succeeded" && taskInfo.num_unsafe_generations < taskInfo.n_variants) {
				displayStatus = "Succeeded";
				borderColor = "#4CAF50"; // Green for success
			} else if (taskInfo.status === "running") {
				displayStatus = "Running";
				borderColor = "#aaa"; // Grey for running
			} else {
				// Check rejection conditions
				if (taskInfo.failure_reason === "input_moderation") {
					displayStatus = "Rejected (Input Filter)";
					borderColor = "#F44336"; // Red for rejection
				} else if (taskInfo.failure_reason === "output_moderation") {
					displayStatus = "Rejected (Output Filter)";
					borderColor = "#F44336"; // Red for rejection
				} else if (
					taskInfo.num_unsafe_generations !== undefined &&
					taskInfo.n_variants !== undefined &&
					taskInfo.num_unsafe_generations >= taskInfo.n_variants
				) {
					displayStatus = "Rejected (Unsafe Filter)";
					borderColor = "#F44336"; // Red for rejection
				} else if (taskInfo.status === "failed") {
					displayStatus = "Failed";
					borderColor = "#F44336"; // Red for failure
				}
			}

			return { displayStatus, borderColor };
		}

		show() {
			this.card.style.display = 'block';
		}

		hide() {
			this.card.style.display = 'none';
		}

		isTaskFailed(taskInfo) {
			const { displayStatus } = this.determineStatusAndColor(taskInfo);
			return taskInfo.status === 'failed' || displayStatus.includes('Rejected');
		}
	}

	/**
	 * Class for displaying retry chain progress
	 */
	class RetryTracker {
		constructor(mainUI) {
			this.mainUI = mainUI;
			this.container = null;
		}

		show(chain) {
			this.remove(); // Remove any existing tracker

			// Create container
			this.container = document.createElement('div');
			this.container.id = 'retry-tracker-container';
			this.container.style.cssText = `
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #444;
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;

			// Progress message
			const progressMessage = document.createElement('div');
			progressMessage.style.color = '#FFC107';
			progressMessage.innerHTML = `<strong>Auto-Retry in progress:</strong> ${chain.current}/${chain.total} attempts remaining`;

			// Stop button
			const stopButton = document.createElement('button');
			stopButton.textContent = 'Stop';
			stopButton.style.cssText = `
            background: #F44336;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            transition: background 0.2s;
        `;

			stopButton.addEventListener('mouseover', () => {
				stopButton.style.background = '#D32F2F';
			});

			stopButton.addEventListener('mouseout', () => {
				stopButton.style.background = '#F44336';
			});

			// Add click handler
			stopButton.addEventListener('click', () => {
				const currentTaskId = getCurrentTaskId();
				RetryChain.removeChainByTaskId(currentTaskId);
				console.log('Auto-retry stopped by user');
				this.remove();

				// Check if we should show the submitter
				if (lastInfo && this.mainUI.isTaskFailed(lastInfo)) {
					const submitter = new RetrySubmitter(this.mainUI);
					submitter.show();
				}
			});

			// Assemble container
			this.container.appendChild(progressMessage);
			this.container.appendChild(stopButton);

			// Add to main UI
			this.mainUI.contentDiv.appendChild(this.container);
		}

		remove() {
			if (this.container && this.container.parentNode) {
				this.container.parentNode.removeChild(this.container);
				this.container = null;
			}
		}
	}

	/**
	 * Class for the UI to submit a new retry
	 */
	class RetrySubmitter {
		constructor(mainUI) {
			this.mainUI = mainUI;
			this.container = null;
		}

		show() {
			this.remove(); // Remove any existing submitter

			// Create container
			this.container = document.createElement('div');
			this.container.id = 'retry-submitter-container';
			this.container.style.cssText = `
				margin-top: 10px;
				padding-top: 10px;
				border-top: 1px solid #444;
				display: flex;
				align-items: center;
				gap: 8px;
			`;

			// Label
			const retryLabel = document.createElement('label');
			retryLabel.textContent = 'Auto-Retry:';

			// Input
			const retryInput = document.createElement('input');
			retryInput.id = 'auto-retry-input';
			retryInput.type = 'number';
			retryInput.min = '1';
			retryInput.max = MAX_AUTO_RETRIES.toString();
			retryInput.value = currentUIRetryCount.toString();
			retryInput.style.cssText = `
				width: 50px;
				background: #333;
				color: white;
				border: 1px solid #555;
				border-radius: 4px;
				padding: 4px;
			`;

			retryInput.addEventListener('input', () => {
				let value = parseInt(retryInput.value);
				if (!isNaN(value)) {
					currentUIRetryCount = value;
				}
			});

			// Clamp input value
			retryInput.addEventListener('blur', () => {
				let value = parseInt(retryInput.value);
				if (isNaN(value) || value < 1) {
					value = 1;
				} else if (value > MAX_AUTO_RETRIES) {
					value = MAX_AUTO_RETRIES;
				}
				retryInput.value = value.toString();
				currentUIRetryCount = value;
			});

			// Button
			const retryButton = document.createElement('button');
			retryButton.textContent = 'Begin';
			retryButton.style.cssText = `
				background: #444;
				color: white;
				border: 1px solid #666;
				border-radius: 4px;
				padding: 4px 8px;
				cursor: pointer;
			`;

			retryButton.addEventListener('mouseover', () => {
				retryButton.style.background = '#555';
			});

			retryButton.addEventListener('mouseout', () => {
				retryButton.style.background = '#444';
			});

			// Click handler for the Begin button
			retryButton.addEventListener('click', async () => {
				// Use the current UI value stored in the global variable
				const count = currentUIRetryCount;
				if (count > 0 && count <= MAX_AUTO_RETRIES) {
					const currentTaskId = getCurrentTaskId();
					let newChain = null;

					try {
						// Fetch the current task's settings
						console.log(`Fetching settings for task ${currentTaskId}`);
						const response = await fetch(`https://sora.com/backend/video_gen/${currentTaskId}`, {
							headers: {
								'Authorization': `Bearer ${authToken}`,
								'Content-Type': 'application/json'
							}
						});

						if (response.ok) {
							const taskInfo = await response.json();

							// Create the retry chain
							newChain = RetryChain.createChain(currentTaskId, count);

							// Extract relevant settings to preserve
							const settings = {
								n_variants: taskInfo.n_variants || 2,
								prompt: taskInfo.prompt,
								height: taskInfo.height,
								width: taskInfo.width,
								operation: taskInfo.operation,
								inpaint_items: taskInfo.inpaint_items,
								remix_config: taskInfo.remix_config,
								model: taskInfo.model
							};

							// Store settings in the chain
							newChain.settings = { ...settings };
							RetryChain.updateChain(newChain);

							console.log('Stored original task settings for retry:', settings);
						} else {
							console.error('Failed to fetch task settings');
							// Fall back to basic chain
							newChain = RetryChain.createChain(currentTaskId, count);
						}
					} catch (error) {
						console.error('Error fetching task settings:', error);
						// Fall back to basic chain
						newChain = RetryChain.createChain(currentTaskId, count);
					} finally {
						if (newChain) {
							// Common UI update code
							this.remove();
							const tracker = new RetryTracker(this.mainUI);
							tracker.show(newChain);

							// Begin retry
							performAutoRetry(true);
						}
					}
				}
			});

			// Assemble container
			this.container.appendChild(retryLabel);
			this.container.appendChild(retryInput);
			this.container.appendChild(retryButton);

			// Add to main UI
			this.mainUI.contentDiv.appendChild(this.container);
		}

		remove() {
			if (this.container && this.container.parentNode) {
				this.container.parentNode.removeChild(this.container);
				this.container = null;
			}
		}
	}

	/**
	 * Class representing an auto-retry chain for Sora generations
	 */
	class RetryChain {
		/**
		 * Create a new retry chain
		 * @param {string} initialTaskId - The first task ID in the chain
		 * @param {number} totalRetries - Total number of retries for this chain
		 */
		constructor(initialTaskId, totalRetries) {
			this.taskIds = [initialTaskId];
			this.generationIds = [];
			this.total = totalRetries;
			this.current = totalRetries;
			this.settings = null;
		}

		/**
		 * Add a new task ID to this chain
		 * @param {string} taskId - The task ID to add
		 */
		addTask(taskId) {
			if (!this.taskIds.includes(taskId)) {
				this.taskIds.push(taskId);
			}
		}

		/**
		 * Add a generation ID to this chain
		 * @param {string} genId - The generation ID to add
		 */
		addGenerationId(genId) {
			if (!this.generationIds.includes(genId)) {
				this.generationIds.push(genId);
			}
		}

		/**
		 * Check if a task ID is part of this chain
		 * @param {string} taskId - Task ID to check
		 * @returns {boolean} True if task is in this chain
		 */
		containsTask(taskId) {
			return this.taskIds.includes(taskId);
		}

		/**
		 * Check if a generation ID is part of this chain
		 * @param {string} genId - Generation ID to check
		 * @returns {boolean} True if generation is in this chain
		 */
		containsGeneration(genId) {
			return this.generationIds.includes(genId);
		}

		/**
		 * Check if this chain has retries remaining
		 * @returns {boolean} True if retries remain
		 */
		hasRetriesLeft() {
			return this.current > 0;
		}

		/**
		 * Get most recent task ID in this chain
		 * @returns {string} Most recent task ID
		 */
		getLatestTaskId() {
			return this.taskIds[this.taskIds.length - 1];
		}

		// Static methods to manage chains in storage

		/**
		 * Save all chains to storage, filtering out expired chains
		 * @param {RetryChain[]} chains - Array of RetryChain objects
		 */
		static saveChains(chains) {
			// Convert the class instances to plain objects for storage
			const plainChains = chains.map(chain => ({
				taskIds: chain.taskIds,
				generationIds: chain.generationIds,
				total: chain.total,
				current: chain.current,
				settings: chain.settings
			}));

			GM_setValue('autoRetryChains', plainChains);
		}

		/**
		 * Load all chains from storage, filtering out expired chains
		 * @returns {RetryChain[]} Array of RetryChain objects
		 */
		static loadChains() {
			const plainChains = GM_getValue('autoRetryChains', []);

			// Convert the plain objects back to class instances, INCLUDING zero length ones.
			return plainChains.map(chain => {
				const newChain = new RetryChain('', 0);
				newChain.taskIds = chain.taskIds;
				newChain.generationIds = chain.generationIds || [];
				newChain.total = chain.total;
				newChain.current = chain.current;
				newChain.settings = chain.settings;
				return newChain;
			});
		}

		/**
		 * Clean up all expired chains (chains with no retries left)
		 * @returns {number} Number of chains removed
		 */
		static cleanupExpiredChains() {
			const chains = RetryChain.loadChains();
			const initialCount = chains.length;

			// Filter to keep only chains with retries left
			const activeChains = chains.filter(chain => chain.current > 0);

			if (activeChains.length !== initialCount) {
				RetryChain.saveChains(activeChains);
				console.log(`Cleaned up ${initialCount - activeChains.length} expired retry chains`);
				return initialCount - activeChains.length;
			}

			return 0;
		}

		/**
		 * Find a chain containing a specific task ID
		 * @param {string} taskId - Task ID to search for
		 * @returns {RetryChain|null} Matching chain or null if not found
		 */
		static findChainByTaskId(taskId) {
			const chains = RetryChain.loadChains();
			return chains.find(chain => chain.containsTask(taskId)) || null;
		}

		/**
		 * Find a chain containing a specific generation ID
		 * @param {string} genId - Generation ID to search for
		 * @returns {RetryChain|null} Matching chain or null if not found
		 */
		static findChainByGenerationId(genId) {
			const chains = RetryChain.loadChains();
			return chains.find(chain => chain.containsGeneration(genId)) || null;
		}

		/**
		 * Create a new chain and save it
		 * @param {string} taskId - Initial task ID for the chain
		 * @param {number} totalRetries - Total retries for this chain
		 * @returns {RetryChain} The newly created chain
		 */
		static createChain(taskId, totalRetries) {
			const chains = RetryChain.loadChains();
			const newChain = new RetryChain(taskId, totalRetries);
			chains.push(newChain);
			RetryChain.saveChains(chains);
			return newChain;
		}

		/**
		 * Remove a chain containing a specific task ID
		 * @param {string} taskId - Task ID in the chain to remove
		 * @returns {boolean} True if a chain was removed
		 */
		static removeChainByTaskId(taskId) {
			const chains = RetryChain.loadChains();
			const initialLength = chains.length;
			const filteredChains = chains.filter(chain => !chain.containsTask(taskId));

			if (filteredChains.length !== initialLength) {
				RetryChain.saveChains(filteredChains);
				return true;
			}
			return false;
		}

		/**
		 * Update an existing chain in storage
		 * @param {RetryChain} updatedChain - The chain with updated values
		 */
		static updateChain(updatedChain) {
			const chains = RetryChain.loadChains();
			const index = chains.findIndex(chain =>
				chain.taskIds.some(id => updatedChain.taskIds.includes(id))
			);

			if (index !== -1) {
				chains[index] = updatedChain;
				RetryChain.saveChains(chains);
			}
		}

		/**
		 * Check if a task ID is the latest in this chain
		 * @param {string} taskId - Task ID to check
		 * @returns {boolean} True if task is the latest in the chain
		 */
		isLatestTask(taskId) {
			if (this.taskIds.length === 0) return false;
			return this.taskIds[this.taskIds.length - 1] === taskId;
		}

		/**
		 * Static method to check if a task is the latest in its chain
		 * @param {string} taskId - Task ID to check
		 * @returns {boolean} True if task is the latest in its chain
		 */
		static isLatestTaskInChain(taskId) {
			const chain = RetryChain.findChainByTaskId(taskId);
			if (!chain) return false;
			return chain.isLatestTask(taskId);
		}
	}

	RetryChain.cleanupExpiredChains();

	function getSuccessesOnlyFilter() {
		return GM_getValue('successesOnlyFilter', false);
	}

	function setSuccessesOnlyFilter(value) {
		GM_setValue('successesOnlyFilter', value);
	}

	// Function to perform auto-retry
	async function performAutoRetry(immediate = false) {
		if (!immediate) await new Promise(r => setTimeout(r, 3000)); // Wait for 3 seconds before retrying

		const currentTaskId = getCurrentTaskId();
		const chain = RetryChain.findChainByTaskId(currentTaskId);

		if (!chain) return; // No chain found for this task

		console.log(`Auto-retry initiated, attempts remaining: ${chain.current}`);

		// Decrement the retry counter IF we're switching chains.
		if (unsafeWindow.currentRetryTaskId != currentTaskId) {
			chain.current--;
			RetryChain.updateChain(chain);
		}


		if (chain.current < 0) {
			RetryChain.removeChainByTaskId(currentTaskId);
			return;
		}

		// Set a temporary flag just for the request interception to work
		unsafeWindow.currentRetryTaskId = currentTaskId;

		try {
			// Step 1: Find and click the Remix button on the main page
			await new Promise(r => setTimeout(r, 3000));
			const remixContainer = document.querySelector('.flex.w-full.items-center.justify-center.tablet\\:w-1\\/2');
			if (remixContainer) {
				const button = remixContainer.querySelector('button');
				if (button) {
					console.log('Found remix button, clicking...');
					button.click();

					// Wait for the dialog to appear
					await new Promise(resolve => setTimeout(resolve, 3000));

					// Step 2: Handle the dialog that appears
					// Case 1: Dialog with a "Remix" button
					const remixButton = Array.from(document.querySelectorAll('button')).find(btn =>
						btn.textContent.trim() === 'Remix' &&
						btn.closest('[role="dialog"]')
					);

					if (remixButton) {
						console.log('Found Remix button in dialog, clicking...');
						remixButton.click();
						// Set a flag to indicate we're in auto-retry mode
						unsafeWindow.popupRetryInProgress = true;
						return;
					}

					// Case 2: Dialog with a "Create image" button (with up arrow SVG)
					const createButtons = document.querySelectorAll('[data-state="closed"] svg');
					for (const svg of createButtons) {
						// Look for the SVG path with the up arrow path data
						const path = svg.querySelector('path[d*="11.293 5.293"]');
						if (path) {
							// Found the create image button
							const createButton = svg.closest('button');
							if (createButton) {
								console.log('Found Create image button in dialog, clicking...');
								createButton.click();
								unsafeWindow.popupRetryInProgress = true;
								return;
							}
						}
					}

					// Case 3: Left for future implementation
					console.log('No recognized dialog buttons found, may need to implement additional case');
				} else {
					console.error('Could not find button in remix container');
				}
			} else {
				console.error('Could not find remix container');
			}
		} catch (error) {
			console.error('Error during auto-retry:', error);
		}

		// If we get here, something went wrong with the retry attempt
		// We'll reload the page anyway as a fallback
		console.log('Auto-retry failed, reloading page...');
		setTimeout(() => {
			window.location.reload();
		}, 1000);
	}

	// Function to show browser notification
	function showNotification(title, message) {
		if (Notification.permission === 'granted') {
			const notification = new Notification(title, {
				body: message,
				icon: 'https://sora.com/favicon.ico' // Sora favicon as icon
			});

			notification.onclick = function () {
				unsafeWindow.focus();
				notification.close();
			};

			// Auto close after 10 seconds
			setTimeout(() => notification.close(), 10000);
		}
	}

	// Create the status card once
	function initUI() {
		if (!mainUI) {
			mainUI = new MainUI();
			retryTracker = new RetryTracker(mainUI);
			retrySubmitter = new RetrySubmitter(mainUI);
		}
		return mainUI;
	}

	function getCurrentTaskId() {
		const match = location.pathname.match(/\/t\/(task_[a-zA-Z0-9]+)/);
		return match ? match[1] : null;
	}

	// Function to update the status card with taskInfo
	function updateStatusCard(taskInfo) {
		const ui = initUI();
		const { displayStatus, borderColor } = ui.updateStatus(taskInfo);

		const currentTaskId = getCurrentTaskId();
		const chain = RetryChain.findChainByTaskId(currentTaskId);
		const isFailed = ui.isTaskFailed(taskInfo);

		// Detect state transitions from running to completed for notifications
		if (lastInfo && lastInfo.status === 'running' && taskInfo.status !== 'running') {
			if (taskInfo.status === 'succeeded' && !isFailed) {
				// Always notify on success
				showNotification(
					'Sora Generation Complete',
					`Your generation is successful! ${taskInfo.id}`
				);

				// If this was part of a chain, remove it
				if (chain) {
					RetryChain.removeChainByTaskId(currentTaskId);
					retryTracker.remove();
				}

				// Reload page after success
				setTimeout(() => {
					window.location.reload();
				}, 1000);
			} else if (isFailed && !chain) {
				// Only notify on failure if not part of a retry chain
				showNotification(
					'Sora Generation Failed',
					`Your generation was ${displayStatus.toLowerCase()}! ${taskInfo.id}`
				);

				// Reload page after failed state with no chain
				setTimeout(() => {
					window.location.reload();
				}, 1000);
			} else if (taskInfo.status == "cancelled") {
				if (chain) {
					RetryChain.removeChainByTaskId(currentTaskId);
					retryTracker.remove();
				}
			}
		}

		// Handle retry UI and retry logic independently of state transitions
		if (chain) {
			// Show retry tracker for any task in a chain
			retryTracker.show(chain);

			// If task has failed and has retries left, initiate a retry
			// This happens regardless of previous state
			if (isFailed && chain.hasRetriesLeft() && !unsafeWindow.currentRetryTaskId) {
				console.log(`Task failed, auto-retrying (${chain.current} left)`);
				performAutoRetry();
			}
		} else if (isFailed) {
			// No chain but task failed, show retry submitter
			retrySubmitter.show();
		} else {
			// Not failed and not in chain, remove both UI elements
			retryTracker.remove();
			retrySubmitter.remove();
		}

		// Store last info
		lastInfo = { ...taskInfo };
		ui.show();
	}

	// Fetch task status directly
	async function fetchTaskStatus(taskId) {
		if (!authToken) {
			console.error('No auth token available for API request');
			return null;
		}

		try {
			const response = await fetch(`https://sora.com/backend/video_gen/${taskId}`, {
				headers: {
					'Authorization': `Bearer ${authToken}`,
					'Content-Type': 'application/json'
				}
			});

			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}

			const taskInfo = await response.json();
			updateStatusCard(taskInfo);
			return taskInfo;
		} catch (error) {
			console.error('Error fetching task status:', error);
			return null;
		}
	}

	// Start polling for task status
	function startTaskPolling(taskId) {
		// Clear any existing polling
		stopTaskPolling();

		// Reset last status when starting new polling
		lastInfo = null;

		// Fetch immediately, then start polling
		fetchTaskStatus(taskId);

		pollingInterval = setInterval(() => {
			fetchTaskStatus(taskId).then(taskInfo => {
				// If task is complete or failed, we can stop polling
				if (taskInfo && (taskInfo.status === 'succeeded' || taskInfo.status === 'failed')) {
					// Continue polling for a short time after completion to ensure we get final status
					setTimeout(() => {
						stopTaskPolling();
					}, 30000); // Keep polling for 30 seconds after completion
				}
			});
		}, 10000); // Poll every 10 seconds
	}

	// Stop polling for task status
	function stopTaskPolling() {
		if (pollingInterval) {
			clearInterval(pollingInterval);
			pollingInterval = null;
		}
	}




	// Filtering changes
	function addSuccessesOnlyFilterOption() {
		// Find the filter menu
		const filterMenu = document.querySelector('[role="menu"][data-radix-menu-content]');
		if (!filterMenu) return false;

		// Check if our option is already added
		if (filterMenu.querySelector('#successes-only-filter')) return true;

		// Create our checkbox item based on the existing ones
		const successesOnlyItem = document.createElement('div');
		successesOnlyItem.id = 'successes-only-filter';
		successesOnlyItem.setAttribute('role', 'menuitemcheckbox');
		successesOnlyItem.setAttribute('aria-checked', getSuccessesOnlyFilter() ? 'true' : 'false');
		successesOnlyItem.setAttribute('data-state', getSuccessesOnlyFilter() ? 'checked' : 'unchecked');
		successesOnlyItem.setAttribute('tabindex', '-1');
		successesOnlyItem.setAttribute('data-orientation', 'vertical');
		successesOnlyItem.setAttribute('data-radix-collection-item', '');
		successesOnlyItem.className = 'group relative flex cursor-default select-none items-center gap-2 rounded-[10px] px-2 py-2.5 text-sm outline-none focus:bg-token-bg-light data-[disabled]:pointer-events-none data-[disabled]:opacity-50';

		// Content
		successesOnlyItem.innerHTML = `
			<div class="flex flex-1 items-center gap-2">
				<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-[18px] w-[18px]">
					<path fill="currentColor" d="M21.043 3.393a2.16 2.16 0 0 1 .523 3.007l-10.25 14.564a2.158 2.158 0 0 1-3.36.21L2.56 15.24a2.158 2.158 0 0 1 3.193-2.903l3.583 3.941 8.7-12.362a2.16 2.16 0 0 1 3.006-.523"></path>
				</svg>Successes Only (SLOW!)
			</div>
			<div class="rounded-md border border-token-bg-active group-data-[state=checked]:bg-token-bg-inverse group-data-[state=checked]:border-token-bg-inverse flex h-4 w-4 items-center justify-center">
				<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-2.5 w-2.5 text-token-bg-primary opacity-0 group-data-[state=checked]:opacity-100">
					<path fill="currentColor" d="M21.043 3.393a2.16 2.16 0 0 1 .523 3.007l-10.25 14.564a2.158 2.158 0 0 1-3.36.21L2.56 15.24a2.158 2.158 0 0 1 3.193-2.903l3.583 3.941 8.7-12.362a2.16 2.16 0 0 1 3.006-.523"></path>
				</svg>
			</div>
		`;

		// Add click handler
		successesOnlyItem.addEventListener('click', function () {
			const isChecked = this.getAttribute('data-state') === 'checked';
			const newState = !isChecked;

			this.setAttribute('data-state', newState ? 'checked' : 'unchecked');
			this.setAttribute('aria-checked', newState ? 'true' : 'false');

			setSuccessesOnlyFilter(newState);

			// Log the change
			console.log('Successes Only filter set to:', newState);

			// Reload the page to apply the filter
			setTimeout(() => {
				window.location.reload();
			}, 100); // Small delay to ensure the setting is saved
		});

		// Add our item to the menu - after all the existing items
		filterMenu.appendChild(successesOnlyItem);

		return true;
	}

	// Function to monitor for the filter menu appearing
	function monitorForFilterMenu() {
		let attempts = 0;
		const maxAttempts = 20; // 20 * 50ms = 1 second max wait time
		let checkInterval;

		function checkForFilterMenu() {
			attempts++;

			if (addSuccessesOnlyFilterOption()) {
				// Success! Menu found and option added
				clearInterval(checkInterval);
			} else if (attempts >= maxAttempts) {
				// Give up after max attempts
				clearInterval(checkInterval);
				console.log('Filter menu not found after maximum attempts');
			}
		}

		checkInterval = setInterval(checkForFilterMenu, 50);
	}



	// Monkeypatch fetch for auto retry and filter
	const originalFetch = unsafeWindow.fetch;

	unsafeWindow.fetch = async function (input, init) {
		// Extract the auth token from outgoing requests
		if (init && init.headers) {
			const authHeader = init.headers.Authorization || init.headers.authorization;
			if (authHeader && authHeader.startsWith('Bearer ')) {
				authToken = authHeader.substring(7); // Remove 'Bearer ' prefix
			}
		}

		// Handle the GET requests to filter library results
		if (typeof input === 'string' &&
			input.includes('backend/video_gen') &&
			input.includes('limit=') &&
			init && init.method === 'GET' && location.href.includes("library")) {

			// Check if Successes Only filter is enabled
			const successesOnlyEnabled = getSuccessesOnlyFilter();

			if (successesOnlyEnabled) {
				const url = new URL("https://sora.com" + input);
				const limitParam = url.searchParams.get('limit');
				const beforeParam = url.searchParams.get('before');
				const afterParam = url.searchParams.get('after');
				const isPaginatingBackwards = Boolean(beforeParam);

				console.log(`Intercepting library request with Successes Only filter enabled. Direction: ${isPaginatingBackwards ? 'backwards' : 'forwards'}`);

				// Make the original request
				const response = await originalFetch(input, init);
				const originalResponseData = await response.clone().json();

				// If there are no task_responses or it's not an array, just return the original response
				if (!originalResponseData.task_responses || !Array.isArray(originalResponseData.task_responses)) {
					return response;
				}

				// Begin collecting successful tasks
				let allSuccessfulTasks = [];
				let currentPaginationId = null;
				let hasMore = originalResponseData.has_more;

				// Add successful tasks from the current response
				const successfulTasks = originalResponseData.task_responses.filter(task =>
					task.status === 'succeeded' &&
					(task.num_unsafe_generations === undefined ||
						task.n_variants === undefined ||
						task.num_unsafe_generations < task.n_variants)
				);

				allSuccessfulTasks = allSuccessfulTasks.concat(successfulTasks);

				// Set the appropriate ID for pagination based on direction
				if (isPaginatingBackwards) {
					currentPaginationId = originalResponseData.first_id;
				} else {
					currentPaginationId = originalResponseData.last_id;
				}

				// If we don't have enough successful tasks (50) and there are more available, fetch more
				while (allSuccessfulTasks.length < 50 && hasMore && currentPaginationId) {
					console.log(`Need more tasks, currently have ${allSuccessfulTasks.length}, fetching more ${isPaginatingBackwards ? 'backwards' : 'forwards'}...`);

					// Construct the URL for the next page based on pagination direction
					const nextPageUrl = new URL(url.origin + url.pathname);
					nextPageUrl.searchParams.set('limit', limitParam || '50');

					if (isPaginatingBackwards) {
						nextPageUrl.searchParams.set('before', currentPaginationId);
					} else {
						nextPageUrl.searchParams.set('after', currentPaginationId);
					}

					try {
						const nextPageResponse = await originalFetch(nextPageUrl.toString(), init);
						const nextPageData = await nextPageResponse.json();

						if (!nextPageData.task_responses || !Array.isArray(nextPageData.task_responses)) {
							break; // Something went wrong, stop fetching
						}

						// Filter for successful tasks
						const nextSuccessfulTasks = nextPageData.task_responses.filter(task =>
							task.status === 'succeeded' &&
							(task.num_unsafe_generations === undefined ||
								task.n_variants === undefined ||
								task.num_unsafe_generations < task.n_variants)
						);

						// Add to our collection
						allSuccessfulTasks = allSuccessfulTasks.concat(nextSuccessfulTasks);

						// Update for next iteration
						hasMore = nextPageData.has_more;

						if (isPaginatingBackwards) {
							currentPaginationId = nextPageData.first_id;
						} else {
							currentPaginationId = nextPageData.last_id;
						}

						if (!hasMore || !currentPaginationId) {
							break; // No more pages to fetch
						}
					} catch (error) {
						console.error('Error fetching additional tasks:', error);
						break;
					}
				}

				// Limit to 50 tasks maximum
				allSuccessfulTasks = allSuccessfulTasks.slice(0, 50);

				console.log(`Returning ${allSuccessfulTasks.length} successful tasks for ${isPaginatingBackwards ? 'backwards' : 'forwards'} pagination`);

				// Create modified response with just the successful tasks
				const modifiedResponse = {
					...originalResponseData,
					task_responses: allSuccessfulTasks,
					// If we have exactly 50 successful tasks and hasMore was true, there might be more
					has_more: allSuccessfulTasks.length >= 50 && hasMore
				};

				// Set the appropriate first_id or last_id based on pagination direction
				if (isPaginatingBackwards && allSuccessfulTasks.length > 0) {
					modifiedResponse.first_id = allSuccessfulTasks[0].id;
				}
				if (!isPaginatingBackwards && allSuccessfulTasks.length > 0) {
					modifiedResponse.last_id = allSuccessfulTasks[allSuccessfulTasks.length - 1].id;
				}

				// Create a new Response object with our modified data
				return new Response(JSON.stringify(modifiedResponse), {
					status: 200,
					statusText: 'OK',
					headers: response.headers
				});
			}
		}

		const chain = (unsafeWindow.currentRetryTaskId) ? RetryChain.findChainByTaskId(unsafeWindow.currentRetryTaskId) : null;
		if (chain) {
			console.log("Found chain:")
			console.log(chain)
		}

		if (typeof input === 'string' &&
			input.includes('/g/gen_') &&
			unsafeWindow.currentRetryTaskId) {
			try {
				// Extract the generation ID from the URL
				const genIdMatch = input.match(/\/g\/(gen_[a-zA-Z0-9]+)/);
				if (genIdMatch && genIdMatch[1]) {
					const genId = genIdMatch[1];
					console.log(`Detected navigation to generation ${genId} during retry of task ${unsafeWindow.currentRetryTaskId}`);
					if (chain) {
						// Add the generation ID to the chain
						chain.addGenerationId(genId);
						RetryChain.updateChain(chain);
						console.log(`Added generation ID ${genId} to retry chain`);

						// Store this for potential URL change handler
						unsafeWindow.lastRetryGenId = genId;
					}
				}
			} catch (error) {
				console.error('Error tracking generation ID:', error);
			}
		}

		// In the fetch monkeypatch
		if (typeof input === 'string' &&
			input.includes('backend/video_gen') &&
			init && init.method === 'POST' &&
			unsafeWindow.currentRetryTaskId) {

			try {
				if (chain) {
					// Get the stored settings
					const settings = chain.settings;
					console.log("Got settings", settings)
					if (settings) {
						// Parse the original body
						const body = JSON.parse(init.body);

						console.log('Original request body:', JSON.stringify(body));

						// Apply all the stored settings from the original task by iterating over properties
						for (const [key, value] of Object.entries(settings)) {
							if (value !== undefined) {
								body[key] = value;
							}
						}
						// Update the request body
						init.body = JSON.stringify(body);

						console.log('Applied original settings to new generation request', JSON.stringify(body));
					}
				} else {
					console.log('No chain found for task ID:', unsafeWindow.currentRetryTaskId);
				}
			} catch (error) {
				console.error('Error applying stored settings to request:', error);
			}
		}

		const response = await originalFetch(input, init);
		const responseClone = response.clone();
		console.log("Current retry task ID:", unsafeWindow.currentRetryTaskId)
		// When capturing the response after a retry
		if (typeof input === 'string' &&
			input.includes('backend/video_gen') &&
			init && init.method === 'POST' &&
			unsafeWindow.currentRetryTaskId) {

			try {
				// Get the body from the response
				const body = await responseClone.json();
				if (body && body.id) {
					const newTaskId = body.id;
					if (chain) {
						// Add the new task to the chain
						chain.addTask(newTaskId);
						RetryChain.updateChain(chain);
					}
					// Clear the temporary flag
					unsafeWindow.currentRetryTaskId = null;

					// Navigate to the new task
					console.log(`Auto-retry successful, navigating to task: ${newTaskId}`);
					unsafeWindow.location.href = `https://sora.com/t/${newTaskId}`;
				}
			} catch (error) {
				console.error('Error capturing new task ID:', error);
			}
		}

		if (typeof input === 'string') {
			try {
				// Check for auth token in response headers (alternative method)
				const authHeader = response.headers.get('Authorization') || response.headers.get('authorization');
				if (authHeader && authHeader.startsWith('Bearer ')) {
					authToken = authHeader.substring(7);
					console.log('Auth token captured from response');
				}

				const currentTaskId = getCurrentTaskId();
				if (currentTaskId) {
					let taskInfo = null;

					if (input.includes('backend/notif')) {
						const response = await responseClone.json();
						if (response && Array.isArray(response.data)) {
							taskInfo = response.data.find(item =>
								item.payload && item.payload.id === currentTaskId
							)?.payload;
						}
					} else if (input.match(/\/backend\/video_gen\/task_[a-zA-Z0-9]+/)) {
						const data = await responseClone.json();
						if (data.id === currentTaskId) {
							taskInfo = data;
						}
					}

					if (taskInfo) {
						updateStatusCard(taskInfo);
					}
				}
			} catch (error) {
				console.error('Error processing response:', error);
			}
		}

		return response;
	};



	// Check URL changes for task retry and filter addon
	setInterval(() => {
		const currentUrl = location.href;
		if (currentUrl !== lastUrl) {
			lastUrl = currentUrl;


			// Reset UI if URL changes
			if (mainUI) {
				// Remove existing UI elements
				const cardElement = document.getElementById('generation-status-card');
				if (cardElement && cardElement.parentNode) {
					cardElement.parentNode.removeChild(cardElement);
				}

				// Reset UI instances
				mainUI = null;
				retryTracker = null;
				retrySubmitter = null;
			}

			// Check if we're on a generation page with remix parameter
			if (currentUrl.includes('/g/gen_')) {
				// Extract the generation ID
				const genIdMatch = currentUrl.match(/\/g\/(gen_[a-zA-Z0-9]+)/);

				if (genIdMatch && genIdMatch[1]) {
					const genId = genIdMatch[1];

					// First, check if this generation is part of any retry chain
					let chain = RetryChain.findChainByGenerationId(genId);

					// If not found but we have a last retry gen ID, use that
					if (!chain && unsafeWindow.lastRetryGenId === genId) {
						// This is a new generation page we just navigated to in a retry
						const sourceTaskId = unsafeWindow.currentRetryTaskId;
						if (sourceTaskId) {
							chain = RetryChain.findChainByTaskId(sourceTaskId);

							if (chain) {
								// Add this generation ID to the chain
								chain.addGenerationId(genId);
								RetryChain.updateChain(chain);
								console.log(`Added generation ID ${genId} to retry chain from URL change handler`);
							}
						}
					}

					// If this generation is part of a retry chain and has the remix parameter
					if (chain && currentUrl.includes('?remix=') && chain.hasRetriesLeft()) {
						console.log('Detected gen page with remix parameter, auto-retry in progress');

						// Look for the Remix button on this page and click it
						setTimeout(() => {
							const remixButtons = Array.from(document.querySelectorAll('button')).filter(btn =>
								btn.textContent.trim() === 'Remix'
							);

							if (remixButtons.length > 0) {
								console.log('Found Remix button on gen page, clicking...');
								remixButtons[0].click();

								// Set the latest task ID for tracking in request interception
								unsafeWindow.currentRetryTaskId = chain.getLatestTaskId();
							} else {
								console.error('Could not find Remix button on gen page');

								// Reload page
								window.location.reload();
							}
						}, 1000); // Wait a second for the page to fully load
					}
				}
			} else {

			}

			// Handle library filter setup
			if (currentUrl.includes('sora.com/library')) {
				console.log('On library page, setting up filter button monitoring');

				// Find the filter button (using the existing setupFilterButtonMonitoring function)
				const setupFilterButtonMonitoring = () => {
					// Look for button with filter SVG path
					const filterButtons = document.querySelectorAll('button');
					let filterButton = null;

					for (const button of filterButtons) {
						// Check for the "Filter" text in screen reader span
						const srSpan = button.querySelector('.sr-only');
						if (srSpan && srSpan.textContent === 'Filter') {
							filterButton = button.closest('[aria-haspopup="menu"]');
							break;
						}
					}

					if (filterButton) {
						console.log("Adding listener...")
						// Add click listener to the filter button
						filterButton.addEventListener('pointerdown', function () {
							console.log('Filter button pointerdown, monitoring for menu');
							monitorForFilterMenu();
						});
					} else {
						// Button not found yet, try again after a short delay
						setTimeout(setupFilterButtonMonitoring, 500);
					}
				};

				setupFilterButtonMonitoring();
			}

			// Get current task ID
			const currentTaskId = getCurrentTaskId();

			if (currentTaskId) {
				// Start polling for this task
				startTaskPolling(currentTaskId);
				RetryChain.cleanupExpiredChains();	//Clean up expired chains
				// Initialize UI
				initUI();

				// Check if this task is part of a retry chain
				const chain = RetryChain.findChainByTaskId(currentTaskId);
				if (chain) {
					// Show retry tracker for this chain
					retryTracker.show(chain);
				}
			} else {
				// Not on a task page, stop polling and hide the card
				stopTaskPolling();
				if (mainUI) {
					mainUI.hide();
				}
			}
		}
	}, 1000);

	console.log('Sora Generation Status Monitor initialized');
})();