// ==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(); }
})();