Model Selector for AI Uncensored (v2.4 - Status Indicator)

Select AI model, recalculates auth headers. UI enhancements + status indicator. Uses unsafeWindow.

// ==UserScript==
// @name         Model Selector for AI Uncensored (v2.4 - Status Indicator)
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Select AI model, recalculates auth headers. UI enhancements + status indicator. Uses unsafeWindow.
// @author       saros
// @match        https://www.aiuncensored.info/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    console.log('[Model Selector] Script starting (v2.4 - Status Indicator)...');

    // --- Configuration ---
    const AVAILABLE_MODELS = [
        "deepseek-ai/DeepSeek-V3-0324",
        "hermes3-405b",
        "hermes3-8b",
        "hermes3-70b",
        "deepseek-r1-671b",
    ];
    const STORAGE_KEY = 'selectedChatModel_AIUncensored_v2';
    const API_ENDPOINT_PATH = '/api/chat';

    // --- Get the initially selected model (or default) ---
    let selectedModel = GM_getValue(STORAGE_KEY, AVAILABLE_MODELS[0]);

    // --- Replicated Header Generation Function (based on site's tF) ---
    const generateAuthHeaders = async (requestBodyObject) => {
        // ... (Header generation logic remains unchanged) ...
        const timestamp = Math.floor(Date.now() / 1e3).toString();
        const bodyString = JSON.stringify(requestBodyObject);
        const payload = `${timestamp}${bodyString}`;
        const encoder = new TextEncoder();
        const secretKey = encoder.encode("your-super-secret-key-replace-in-production");
        const dataToSign = encoder.encode(payload);
        try {
            const importedKey = await crypto.subtle.importKey("raw", secretKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
            const signatureBuffer = await crypto.subtle.sign("HMAC", importedKey, dataToSign);
            const signatureHex = Array.from(new Uint8Array(signatureBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
            return {
                "X-API-Key": "62852b00cb9e44bca86f0ec7e7455dc6", "X-Timestamp": timestamp, "X-Signature": signatureHex,
                "Content-Type": "application/json", "accept": "*/*", "accept-language": "en-US,en;q=0.9",
            };
        } catch (error) { console.error("[Model Selector] Error generating signature:", error); throw error; }
    };

    // --- Function to Update Status Display (accessible within IIFE) ---
    function updateStatusDisplay(type, messageContent = '') {
        const indicator = document.getElementById('modelStatusIndicator');
        if (!indicator) return;

        let messagePrefix = '';
        indicator.className = 'status-indicator-base'; // Base class

        const modelName = messageContent.includes('/') ? messageContent.split('/')[1] : messageContent;

        switch (type) {
            case 'success':
                messagePrefix = 'Using: ';
                indicator.classList.add('status-success');
                indicator.textContent = messagePrefix + modelName;
                break;
            case 'no-change':
                messagePrefix = 'Already: ';
                indicator.classList.add('status-no-change');
                indicator.textContent = messagePrefix + modelName;
                break;
            case 'warning':
                messagePrefix = 'Warning: ';
                indicator.classList.add('status-warning');
                indicator.textContent = messagePrefix + (messageContent || 'Check console.');
                break;
            case 'error':
                messagePrefix = 'Error: ';
                indicator.classList.add('status-error');
                indicator.textContent = messagePrefix + (messageContent || 'Override failed. Check console.');
                break;
            case 'selected':
                messagePrefix = 'Selected: ';
                indicator.classList.add('status-selected');
                indicator.textContent = messagePrefix + modelName + ". Awaiting API call.";
                break;
            case 'idle':
            default:
                indicator.classList.add('status-idle');
                indicator.textContent = messageContent || 'Panel active. Status updates after API usage.';
                break;
        }
    }

    // --- Intercept Fetch using unsafeWindow ---
    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function(input, init) {
        const url = (input instanceof Request) ? input.url : input;
        const method = ((init && init.method) || (input instanceof Request && input.method) || 'GET').toUpperCase();

        if (url.endsWith(API_ENDPOINT_PATH) && method === 'POST' && init && init.body) {
            try {
                let bodyData = JSON.parse(init.body);
                if (bodyData && typeof bodyData === 'object' && bodyData.hasOwnProperty('model')) {
                    if (bodyData.model !== selectedModel) {
                        console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Original model: ${bodyData.model}`);
                        bodyData.model = selectedModel;
                        console.log(`[Model Selector] Modified model to: ${selectedModel}`);
                        console.log('[Model Selector] Regenerating auth headers...');
                        const newHeaders = await generateAuthHeaders(bodyData);
                        updateStatusDisplay('success', selectedModel); // Update status: SUCCESS
                        const newInit = { ...init, headers: newHeaders, body: JSON.stringify(bodyData) };
                        console.log('[Model Selector] Sending fetch with new headers and modified body.');
                        return originalFetch(input, newInit);
                    } else {
                        console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Model already matches selection (${selectedModel}). Passing through.`);
                        updateStatusDisplay('no-change', selectedModel); // Update status: NO CHANGE
                    }
                } else {
                     console.warn('[Model Selector] Fetch body did not contain expected "model" property:', bodyData);
                     updateStatusDisplay('warning', 'Payload issue'); // Update status: WARNING
                }
            } catch (e) {
                console.error('[Model Selector] Error processing fetch interceptor:', e);
                updateStatusDisplay('error', 'Processing error'); // Update status: ERROR
            }
        }
        return originalFetch(input, init);
    };

    // --- Create Improved UI ---
    function createEnhancedUI() {
        console.log('[Model Selector] createEnhancedUI called');

        GM_addStyle(`
            /* ... (all previous styles for toggle button, container, header, body, label, dropdown remain the same) ... */
            #modelSelectorToggleBtn {
                position: fixed; top: 16px; left: 60px; z-index: 9998; background-color: #2a2a2a;
                color: #e0e0e0; border: 1px solid #c15a17; border-radius: 50%; width: 40px; height: 40px;
                font-size: 22px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.35);
                transition: background-color 0.2s, transform 0.2s, border-color 0.3s ease;
                display: flex; align-items: center; justify-content: center; line-height: 1;
            }
            #modelSelectorToggleBtn:hover { background-color: #383838; transform: translateY(-1px) scale(1.05); }

            #modelSelectorContainer {
                position: fixed; top: 70px; left: 15px; z-index: 9999; background-color: #1e1e1e;
                border: 1px solid #c15a17; padding: 0; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.6);
                font-family: 'Segoe UI', 'Avenir', 'Arial', sans-serif; color: #e0e0e0; min-width: 290px;
                display: flex; flex-direction: column; opacity: 0; transform: translateY(-15px) scale(0.98);
                visibility: hidden; transition: opacity 0.25s ease-out, transform 0.25s ease-out, visibility 0s linear 0.25s;
            }
            #modelSelectorContainer.visible { opacity: 1; transform: translateY(0) scale(1); visibility: visible; transition-delay: 0s; }

            #modelSelectorHeader {
                background-color: #282828; padding: 10px 15px; border-bottom: 1px solid #c15a17;
                border-radius: 9px 9px 0 0; display: flex; justify-content: space-between; align-items: center;
                font-weight: 600; font-size: 15px;
            }
            #modelSelectorHeader span { color: #c15a17; font-weight: 700; }
            #modelSelectorCloseBtn {
                background: none; border: none; color: #b0b0b0; font-size: 26px; font-weight: bold;
                cursor: pointer; padding: 0 5px; line-height: 1; opacity: 0.7; transition: opacity 0.2s, color 0.2s;
            }
            #modelSelectorCloseBtn:hover { opacity: 1; color: #ffffff; }

            #modelSelectorBody { padding: 18px; display: flex; flex-direction: column; gap: 10px; }
            #modelSelectorContainer label { margin: 0 0 2px 0; font-weight: 500; color: #cccccc; font-size: 13.5px; }

            #modelSelectorDropdown {
                width: 100%; padding: 10px 12px; background-color: #2c2c2c; color: #e0e0e0;
                border: 1px solid #555; border-radius: 6px; font-size: 14px; box-sizing: border-box;
                transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
            }
            #modelSelectorDropdown:hover { border-color: #777; }
            #modelSelectorDropdown:focus { border-color: #c15a17; box-shadow: 0 0 0 2px rgba(193, 90, 23, 0.3); outline: none; }
            #modelSelectorDropdown option { background-color: #2c2c2c; color: #e0e0e0; padding: 8px 10px; }

            /* --- NEW: Status Indicator Styles --- */
            .status-indicator-base { /* Renamed from #modelStatusIndicator for more general class use */
                padding: 8px 10px;
                margin-top: 12px; /* Increased margin a bit */
                border-radius: 5px; /* Slightly more rounded */
                font-size: 12.5px; /* Slightly larger */
                text-align: center;
                transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
                border: 1px solid transparent; /* Base border */
                line-height: 1.4;
            }
            .status-indicator-base.status-idle,
            .status-indicator-base.status-selected {
                background-color: #303030;
                color: #b0b0b0;
                border-color: #404040;
            }
            .status-indicator-base.status-success {
                background-color: #203c20; /* Darker, less saturated green */
                color: #90c090; /* Softer green text */
                border-color: #305c30;
            }
            .status-indicator-base.status-no-change {
                background-color: #22303f; /* Darker, less saturated blue */
                color: #8ab0d0; /* Softer blue text */
                border-color: #32506f;
            }
            .status-indicator-base.status-warning {
                background-color: #3f3f22; /* Darker, less saturated yellow */
                color: #c0c08a; /* Softer yellow text */
                border-color: #5f5f32;
            }
            .status-indicator-base.status-error {
                background-color: #3f2222; /* Darker, less saturated red */
                color: #d08a8a; /* Softer red text */
                border-color: #5f3232;
            }
        `);

        const toggleBtn = document.createElement('button'); /* ... */
        toggleBtn.id = 'modelSelectorToggleBtn'; toggleBtn.innerHTML = '⚙️'; toggleBtn.title = 'Toggle Model Selector';
        const container = document.createElement('div'); container.id = 'modelSelectorContainer';
        const header = document.createElement('div'); header.id = 'modelSelectorHeader';
        const title = document.createElement('span'); title.textContent = 'Model Settings';
        const closeBtn = document.createElement('button'); closeBtn.id = 'modelSelectorCloseBtn'; closeBtn.innerHTML = '×'; closeBtn.title = 'Close Settings';
        header.appendChild(title); header.appendChild(closeBtn);
        const bodyEl = document.createElement('div'); bodyEl.id = 'modelSelectorBody'; // Renamed to avoid conflict with global 'body'
        const label = document.createElement('label'); label.htmlFor = 'modelSelectorDropdown'; label.textContent = 'Active AI Model:';
        const select = document.createElement('select'); select.id = 'modelSelectorDropdown';
        AVAILABLE_MODELS.forEach(model => { /* ... option population ... */
            const option = document.createElement('option'); option.value = model;
            option.textContent = model.includes('/') ? model.split('/')[1] : model; option.title = model;
            if (model === selectedModel) { option.selected = true; } select.appendChild(option);
        });
        bodyEl.appendChild(label); bodyEl.appendChild(select);

        // --- Create Status Indicator Element ---
        const statusIndicator = document.createElement('div');
        statusIndicator.id = 'modelStatusIndicator'; // Used by getElementById
        // Initial status set after appending, or by calling updateStatusDisplay
        bodyEl.appendChild(statusIndicator); // Add it to the panel body

        container.appendChild(header); container.appendChild(bodyEl); // Use bodyEl

        function appendElements() { /* ... (same appendElements logic) ... */
            if (document.body) { document.body.appendChild(toggleBtn); document.body.appendChild(container);
                updateStatusDisplay('idle'); // Set initial status message once UI is in DOM
            } else { window.addEventListener('DOMContentLoaded', () => {
                document.body.appendChild(toggleBtn); document.body.appendChild(container);
                updateStatusDisplay('idle'); // Set initial status message
            });}}
        if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', appendElements); } else { appendElements(); }

        // --- Event Listeners ---
        toggleBtn.addEventListener('click', () => { container.classList.toggle('visible'); });
        closeBtn.addEventListener('click', () => { container.classList.remove('visible'); });

        select.addEventListener('change', (event) => {
            selectedModel = event.target.value;
            GM_setValue(STORAGE_KEY, selectedModel);
            console.log(`[Model Selector] Model selection changed to: ${selectedModel}`);

            updateStatusDisplay('selected', selectedModel); // Update status for selection

            toggleBtn.style.borderColor = '#4caf50';
            setTimeout(() => { toggleBtn.style.borderColor = '#c15a17'; }, 600);
            select.style.transition = 'none'; select.style.borderColor = '#4caf50'; select.style.backgroundColor = '#3a4a3a';
            setTimeout(() => {
                select.style.transition = 'border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease';
                select.style.borderColor = '#555'; select.style.backgroundColor = '#2c2c2c';
            }, 500);
        });
    }

    // --- Initialize UI ---
    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createEnhancedUI); } else { createEnhancedUI(); }

})();