AI Studio Status Notifier

Adds a running timer to the tab title during generation and provides a visual notification on completion. Refined for efficiency.

// ==UserScript==
// @name         AI Studio Status Notifier
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description  Adds a running timer to the tab title during generation and provides a visual notification on completion. Refined for efficiency.
// @author       AI: Google's Gemini Model
// @match        https://aistudio.google.com/*
// @icon         https://www.gstatic.com/aistudio/ai_studio_favicon_64x64.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Singleton Check ---
    if (window.aiStudioNotifierInitialized) {
        console.log("[AI Studio Status Notifier] Already initialized. Skipping redundant execution.");
        return;
    }
    window.aiStudioNotifierInitialized = true;

    // --- Configuration ---
    const DEBUG = false; // Set to true to enable verbose logging for debugging.
    const runButtonSelector = 'run-button button';
    const stoppableClass = 'stoppable';

    let originalTitle = document.title || "AI Studio";
    let currentState = 'ready'; // 'ready' or 'running'
    let timerInterval = null;
    let startTime = 0;

    // --- Logging ---
    function log(message, level = 'info') {
        if (DEBUG || level === 'info') { // Simplified condition
            console.log(`[AI Studio Status Notifier v1.2.0] ${message}`);
        }
    }

    // --- Notification ---
    function showVisualNotification() {
        log('Displaying visual notification.');
        const flash = document.createElement('div');
        flash.style.position = 'fixed';
        flash.style.top = '0';
        flash.style.left = '0';
        flash.style.width = '100vw';
        flash.style.height = '100vh';
        flash.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
        flash.style.zIndex = '99999';
        flash.style.pointerEvents = 'none';
        flash.style.transition = 'opacity 200ms ease-out';
        document.body.appendChild(flash);
        setTimeout(() => {
            flash.style.opacity = '0';
            setTimeout(() => flash.remove(), 200);
        }, 150);
    }

    // --- DOM Manipulation ---
    function setTitlePrefix(prefix) {
        const newTitle = prefix ? `${prefix} ${originalTitle}` : originalTitle;
        if (document.title !== newTitle) {
            log(`Setting tab title to: ${newTitle}`, 'debug');
            document.title = newTitle;
        }
    }

    // --- Timer Logic ---
    function startTimer() {
        if (timerInterval) clearInterval(timerInterval);
        log('Starting title timer.');
        startTime = Date.now();
        timerInterval = setInterval(() => {
            const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
            const minutes = Math.floor(elapsedSeconds / 60).toString().padStart(2, '0');
            const seconds = (elapsedSeconds % 60).toString().padStart(2, '0');
            setTitlePrefix(`Running [${minutes}:${seconds}] |`);
        }, 1000);
    }

    function stopTimer() {
        if (timerInterval) {
            log('Stopping title timer.');
            clearInterval(timerInterval);
            timerInterval = null;
        }
    }

    // --- Core Logic ---
    const observerCallback = function(mutationsList, observer) {
        if (!originalTitle || originalTitle === "") {
            if (document.title && document.title !== "") {
                originalTitle = document.title;
                log(`Stable original title captured: "${originalTitle}"`);
            }
        }

        const runButton = document.querySelector(runButtonSelector);
        if (!runButton) return;

        const isStoppable = runButton.classList.contains(stoppableClass);

        if (isStoppable && currentState === 'ready') {
            log("State changing to 'running' based on run button.");
            currentState = 'running';
            startTimer();
        } else if (!isStoppable && currentState === 'running') {
            log("State changing to 'ready' based on run button.");
            currentState = 'ready';
            stopTimer();
            setTitlePrefix('Ready |');
            // MODIFICATION: Removed the redundant 'playNotification' function and call 'showVisualNotification' directly.
            showVisualNotification();
        }
    };

    // --- Initialization ---
    function initialize() {
        log('Initializing AI Studio Status Notifier v1.2.0...');
        const targetNode = document.body;
        const config = {
            attributes: true,
            attributeFilter: ['class'],
            childList: true,
            subtree: true
        };
        const observer = new MutationObserver(observerCallback);
        observer.observe(targetNode, config);
        log('Observer is now watching for run button state changes.');
    }

    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize);
    }
})();