您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Select AI model, recalculates auth headers. Auto-syncs with site's Fast/Ultra mode. Ultra mode only shows reasoning models. UI enhancements + status indicator. Uses unsafeWindow.
// ==UserScript== // @name Model Selector for AI Uncensored (v2.7 - Strict Ultra Mode) // @namespace http://tampermonkey.net/ // @version 2.7 // @description Select AI model, recalculates auth headers. Auto-syncs with site's Fast/Ultra mode. Ultra mode only shows reasoning models. 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.7 - Strict Ultra Mode)...'); // --- Configuration --- // Define models with their types const AVAILABLE_MODELS_DATA = [ { id: "deepseek-ai/DeepSeek-V3-0324", name: "DeepSeek-V3-0324", type: "normal" }, { id: "hermes3-405b", name: "Hermes3-405b", type: "normal" }, { id: "hermes3-8b", name: "Hermes3-8b", type: "normal" }, { id: "hermes3-70b", name: "Hermes3-70b", type: "normal" }, { id: "deepseek-ai/DeepSeek-R1-0528", name: "DeepSeek-R1-0528", type: "reasoning" }, { id: "deepseek-r1-671b", name: "DeepSeek-R1-671b", type: "reasoning" }, ]; // Pre-filter models for convenience based on internal 'type' const NORMAL_MODELS = AVAILABLE_MODELS_DATA.filter(m => m.type === 'normal'); const REASONING_MODELS = AVAILABLE_MODELS_DATA.filter(m => m.type === 'reasoning'); const STORAGE_KEY_MODEL = 'selectedChatModel_AIUncensored_v2'; const STORAGE_KEY_DISPLAY_MODE = 'displayMode_AIUncensored_v2'; // 'normal' or 'ultra' const API_ENDPOINT_PATH = '/api/chat'; // --- Global Variables (will be set by UI creation) --- let selectedModel = GM_getValue(STORAGE_KEY_MODEL, NORMAL_MODELS[0]?.id || AVAILABLE_MODELS_DATA[0]?.id); let currentDisplayMode = GM_getValue(STORAGE_KEY_DISPLAY_MODE, 'normal'); // Default to 'normal' let selectDropdown; // Reference to the model selection dropdown let normalRadioBtn; // Reference to the 'Normal' radio button let ultraRadioBtn; // Reference to the 'Ultra' radio button // --- Functions to update UI elements (declared early for accessibility) --- // Function to ensure selected model is compatible with the current display mode function ensureModelCompatibility() { let compatibleModels; if (currentDisplayMode === 'normal') { compatibleModels = NORMAL_MODELS; } else { // 'ultra' mode compatibleModels = REASONING_MODELS; // Ultra mode ONLY allows reasoning models } const modelExistsInCurrentMode = compatibleModels.some(m => m.id === selectedModel); if (!modelExistsInCurrentMode) { // If the previously selected model isn't available in the current mode, // default to the first available model for this mode. selectedModel = compatibleModels[0]?.id || AVAILABLE_MODELS_DATA[0]?.id || null; // Fallback to first available overall, or null if (selectedModel) { GM_setValue(STORAGE_KEY_MODEL, selectedModel); console.log(`[Model Selector] Adjusted selected model to '${selectedModel}' for '${currentDisplayMode}' mode compatibility.`); } else { GM_deleteValue(STORAGE_KEY_MODEL); // Clear if no models are available for this mode console.warn(`[Model Selector] No compatible models found for '${currentDisplayMode}' mode. Selected model set to null.`); } } } // Function to update the model dropdown options based on currentDisplayMode function updateModelDropdownOptions() { if (!selectDropdown) return; // Clear existing options selectDropdown.innerHTML = ''; let modelsToDisplay; if (currentDisplayMode === 'normal') { modelsToDisplay = NORMAL_MODELS; if (modelsToDisplay.length === 0) { console.warn('[Model Selector] No "normal" models configured. Please check AVAILABLE_MODELS_DATA.'); } } else { // 'ultra' // --- THIS IS THE KEY CHANGE --- modelsToDisplay = REASONING_MODELS; // Only show reasoning models for Ultra mode if (modelsToDisplay.length === 0) { console.warn('[Model Selector] No "reasoning" models configured for Ultra mode. Please check AVAILABLE_MODELS_DATA.'); } } let newSelectedModelAvailable = false; if (modelsToDisplay.length > 0) { modelsToDisplay.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; option.title = model.id; // Full ID as title for hover if (model.id === selectedModel) { option.selected = true; newSelectedModelAvailable = true; } selectDropdown.appendChild(option); }); // If the previously selected model is no longer available in the new mode, // default to the first available model in the new mode. if (!newSelectedModelAvailable) { selectedModel = modelsToDisplay[0].id; GM_setValue(STORAGE_KEY_MODEL, selectedModel); selectDropdown.value = selectedModel; // Update dropdown selection console.log(`[Model Selector] Adjusted selected model to '${selectedModel}' for new mode compatibility.`); } } else { selectedModel = null; // No models available for this mode GM_deleteValue(STORAGE_KEY_MODEL); // Clear if no models const noModelOption = document.createElement('option'); noModelOption.textContent = "No models available for this mode."; noModelOption.value = ""; noModelOption.disabled = true; noModelOption.selected = true; selectDropdown.appendChild(noModelOption); console.warn("[Model Selector] No models available for the current display mode!"); } // Always update status after dropdown is populated/adjusted updateStatusDisplay('idle', selectedModel); } // Helper function to update our custom radio buttons function updateRadioButtons() { if (normalRadioBtn && ultraRadioBtn) { normalRadioBtn.checked = (currentDisplayMode === 'normal'); ultraRadioBtn.checked = (currentDisplayMode === 'ultra'); } } // --- Replicated Header Generation Function (based on site's tF) --- const generateAuthHeaders = async (requestBodyObject) => { 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"); // This exact key needs to match the site's. 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 --- function updateStatusDisplay(type, messageContent = '') { const indicator = document.getElementById('modelStatusIndicator'); if (!indicator) return; let messagePrefix = ''; indicator.className = 'status-indicator-base'; // Base class for styling let modelDisplayName = messageContent; // Find the friendly name for the model ID const modelObj = AVAILABLE_MODELS_DATA.find(m => m.id === messageContent); if (modelObj) { modelDisplayName = modelObj.name; } else if (messageContent === null || messageContent === '') { modelDisplayName = 'No model selected'; // Case for no compatible models } const modeText = currentDisplayMode === 'ultra' ? 'Ultra Mode' : 'Normal Mode'; switch (type) { case 'success': messagePrefix = 'Using: '; indicator.classList.add('status-success'); indicator.textContent = `${messagePrefix}${modelDisplayName} (${modeText})`; break; case 'no-change': messagePrefix = 'Already: '; indicator.classList.add('status-no-change'); indicator.textContent = `${messagePrefix}${modelDisplayName} (${modeText})`; 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}${modelDisplayName} (${modeText}). Awaiting API call.`; break; case 'idle': default: indicator.classList.add('status-idle'); indicator.textContent = `Panel active. Current: ${modelDisplayName} (${modeText}). 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); // Ensure the body has a 'model' property before attempting to modify if (bodyData && typeof bodyData === 'object' && bodyData.hasOwnProperty('model')) { // Only modify if the current model in the request doesn't match our selection if (bodyData.model !== selectedModel) { console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Original model: ${bodyData.model}`); if (selectedModel) { // Only attempt to override if a model is actually selected bodyData.model = selectedModel; // Apply the user's selected model console.log(`[Model Selector] Modified model to: ${selectedModel}`); console.log('[Model Selector] Regenerating auth headers...'); const newHeaders = await generateAuthHeaders(bodyData); // Regenerate headers with the new body 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.warn('[Model Selector] No model selected in userscript dropdown. Allowing original request with site\'s chosen model.'); updateStatusDisplay('warning', 'No model selected in panel. Original request sent.'); } } 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 or was malformed:', bodyData); updateStatusDisplay('warning', 'Payload issue'); // Update status: WARNING } } catch (e) { console.error('[Model Selector] Error processing fetch interceptor:', e); updateStatusDisplay('error', `Processing error: ${e.message}`); // Update status: ERROR } } return originalFetch(input, init); // For all other requests, pass them through unchanged }; // --- Observer for site's Fast/Ultra buttons --- function setupModeObserver() { // The container for Fast/Ultra/Call buttons. // Based on the provided JS snippet, this is `tF` styled component, which is a `div` // positioned fixed at the bottom. This selector is robust to class name changes from build. const chatModeContainerSelector = 'div[style*="bottom: 120px"][style*="left: 50%"][style*="fixed"]'; const observerTarget = document.querySelector(chatModeContainerSelector); if (!observerTarget) { // Element not found immediately, retry after a short delay. // This is crucial because the script runs at document-start, but elements // might be rendered later by the SPA framework. console.log('[Model Selector] Chat mode container not found, retrying setupModeObserver in 500ms...'); setTimeout(setupModeObserver, 500); return; } console.log('[Model Selector] Chat mode container found, setting up MutationObserver.'); const buttons = observerTarget.querySelectorAll('button'); let initialModeDetected = false; // --- Initial check for active mode when script loads --- buttons.forEach(button => { const buttonText = button.textContent.trim(); const computedStyle = window.getComputedStyle(button); // Check for the specific active background color (#c15a17 in RGB) if (computedStyle.backgroundColor === 'rgb(193, 90, 23)') { // This is the color for active buttons if (buttonText === 'Fast') { currentDisplayMode = 'normal'; initialModeDetected = true; } else if (buttonText === 'Ultra') { // Site's "beta" corresponds to our "ultra" currentDisplayMode = 'ultra'; initialModeDetected = true; } } }); if (initialModeDetected) { GM_setValue(STORAGE_KEY_DISPLAY_MODE, currentDisplayMode); console.log(`[Model Selector] Initial site mode detected: ${currentDisplayMode}`); } else { console.warn('[Model Selector] No active site mode button found on initial load. Defaulting to stored mode.'); } // Run compatibility check and update UI based on (potentially new) currentDisplayMode ensureModelCompatibility(); // Ensure selectedModel is valid for this mode updateRadioButtons(); // Update our custom radio buttons updateModelDropdownOptions(); // Populate model dropdown based on the mode updateStatusDisplay('idle', selectedModel); // Initial status message // --- MutationObserver to watch for changes --- const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { // We are only interested in attribute changes on the buttons themselves if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const targetButton = mutation.target; // The button whose class attribute changed const buttonText = targetButton.textContent.trim(); const computedStyle = window.getComputedStyle(targetButton); // Check if the button currently has the active background color const isActive = computedStyle.backgroundColor === 'rgb(193, 90, 23)'; let newMode = currentDisplayMode; // Assume no change if (buttonText === 'Fast' && isActive) { newMode = 'normal'; } else if (buttonText === 'Ultra' && isActive) { // Site's "beta" corresponds to our "ultra" newMode = 'ultra'; } // If a different mode is now active, update our internal state and UI if (newMode !== currentDisplayMode) { currentDisplayMode = newMode; GM_setValue(STORAGE_KEY_DISPLAY_MODE, currentDisplayMode); console.log(`[Model Selector] Site mode changed by user interaction to: ${currentDisplayMode}`); // Update our custom UI elements to reflect the new mode ensureModelCompatibility(); // Re-ensure model compatibility after mode change updateRadioButtons(); updateModelDropdownOptions(); updateStatusDisplay('selected', selectedModel); // Indicate new selection } } } }); // Observe each mode button for changes to its class attribute. // This is important because the active state changes by adding/removing classes. buttons.forEach(button => { observer.observe(button, { attributes: true, attributeFilter: ['class'] }); }); } // --- Create Enhanced UI --- function createEnhancedUI() { console.log('[Model Selector] createEnhancedUI called'); GM_addStyle(` #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; } /* --- Status Indicator Styles --- */ .status-indicator-base { padding: 8px 10px; margin-top: 12px; border-radius: 5px; font-size: 12.5px; text-align: center; transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; border: 1px solid transparent; 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; } /* --- Display Mode Styles --- */ #displayModeSection { margin-top: 10px; padding-top: 10px; border-top: 1px dashed #3a3a3a; /* Separator for display mode */ } #displayModeSection label { margin-bottom: 5px; } #displayModeOptions { display: flex; gap: 15px; /* Space between radio buttons */ justify-content: center; margin-top: 5px; } #displayModeOptions input[type="radio"] { display: none; /* Hide native radio button */ } #displayModeOptions label.radio-btn { display: inline-block; padding: 8px 15px; border: 1px solid #555; border-radius: 5px; cursor: pointer; background-color: #2c2c2c; color: #e0e0e0; font-size: 13px; transition: background-color 0.2s, border-color 0.2s, color 0.2s; user-select: none; /* Prevent text selection */ } #displayModeOptions input[type="radio"]:checked + label.radio-btn { background-color: #c15a17; /* Highlight checked button */ border-color: #c15a17; color: #ffffff; box-shadow: 0 0 0 2px rgba(193, 90, 23, 0.3); } #displayModeOptions label.radio-btn:hover { background-color: #3a3a3a; border-color: #777; } `); // --- UI Elements Creation --- 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'; // --- Display Mode Selection Section --- const displayModeSection = document.createElement('div'); displayModeSection.id = 'displayModeSection'; const displayModeLabel = document.createElement('label'); displayModeLabel.textContent = 'Chat Display Mode:'; const displayModeOptions = document.createElement('div'); displayModeOptions.id = 'displayModeOptions'; const createRadioButton = (id, value, text) => { const radioInput = document.createElement('input'); radioInput.type = 'radio'; radioInput.id = id; radioInput.name = 'displayMode'; // All radios in group must have same name radioInput.value = value; // Checked state set by updateRadioButtons() later const radioLabel = document.createElement('label'); radioLabel.htmlFor = id; radioLabel.textContent = text; radioLabel.classList.add('radio-btn'); radioInput.addEventListener('change', (event) => { const newMode = event.target.value; if (newMode !== currentDisplayMode) { currentDisplayMode = newMode; GM_setValue(STORAGE_KEY_DISPLAY_MODE, currentDisplayMode); console.log(`[Model Selector] Custom mode selection changed to: ${currentDisplayMode}`); ensureModelCompatibility(); // Re-ensure model compatibility after mode change updateModelDropdownOptions(); // Crucial: update model list based on new mode updateStatusDisplay('selected', selectedModel); // Indicate new selection } }); displayModeOptions.appendChild(radioInput); displayModeOptions.appendChild(radioLabel); return radioInput; // Return the input element for global reference }; // Create the 'Normal' and 'Ultra' radio buttons and store references normalRadioBtn = createRadioButton('displayModeNormal', 'normal', 'Normal'); ultraRadioBtn = createRadioButton('displayModeUltra', 'ultra', 'Ultra (Reasoning)'); displayModeSection.appendChild(displayModeLabel); displayModeSection.appendChild(displayModeOptions); bodyEl.appendChild(displayModeSection); // --- Model Dropdown Section --- const modelLabel = document.createElement('label'); modelLabel.htmlFor = 'modelSelectorDropdown'; modelLabel.textContent = 'Active AI Model:'; selectDropdown = document.createElement('select'); // Assign to global variable selectDropdown.id = 'modelSelectorDropdown'; bodyEl.appendChild(modelLabel); bodyEl.appendChild(selectDropdown); // --- Status Indicator Element --- const statusIndicator = document.createElement('div'); statusIndicator.id = 'modelStatusIndicator'; bodyEl.appendChild(statusIndicator); // --- Assemble Container --- container.appendChild(header); container.appendChild(bodyEl); // --- Append elements to the DOM --- function appendElements() { if (document.body) { document.body.appendChild(toggleBtn); document.body.appendChild(container); // Now that elements are in DOM, set up the observer and initial UI states setupModeObserver(); // This will also call updateRadioButtons and updateModelDropdownOptions } else { // Fallback for slower DOM loading window.addEventListener('DOMContentLoaded', () => { document.body.appendChild(toggleBtn); document.body.appendChild(container); setupModeObserver(); }); } } // Check document state for immediate or delayed appending if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', appendElements); } else { appendElements(); } // --- Event Listeners for custom UI --- toggleBtn.addEventListener('click', () => { container.classList.toggle('visible'); }); closeBtn.addEventListener('click', () => { container.classList.remove('visible'); }); selectDropdown.addEventListener('change', (event) => { selectedModel = event.target.value; GM_setValue(STORAGE_KEY_MODEL, selectedModel); console.log(`[Model Selector] Model selection changed to: ${selectedModel}`); updateStatusDisplay('selected', selectedModel); // Update status for selection // Visual feedback for selection change toggleBtn.style.borderColor = '#4caf50'; // Green border on toggle button setTimeout(() => { toggleBtn.style.borderColor = '#c15a17'; }, 600); // Revert after a short delay selectDropdown.style.transition = 'none'; // Temporarily disable transition for immediate color change selectDropdown.style.borderColor = '#4caf50'; // Green border on dropdown selectDropdown.style.backgroundColor = '#3a4a3a'; // Darker background setTimeout(() => { // Re-enable transition and revert styles selectDropdown.style.transition = 'border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease'; selectDropdown.style.borderColor = '#555'; selectDropdown.style.backgroundColor = '#2c2c2c'; }, 500); }); } // --- Initialize UI --- // Ensure UI creation happens after the DOM is ready, or immediately if it already is. // setupModeObserver will be called from createEnhancedUI after elements are appended. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createEnhancedUI); } else { createEnhancedUI(); } })();