// ==UserScript==
// @name Model Selector for AI Uncensored (v2.2 - Header Recalc)
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Select AI model, recalculates auth headers after modification. 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.2)...');
// --- Configuration ---
const AVAILABLE_MODELS = [
"deepseek-ai/DeepSeek-V3-0324", // Default from the snippet
"hermes3-405b",
"hermes3-8b",
"hermes3-70b",
"deepseek-r1-671b", // Beta model from snippet
// Add more model identifiers as needed
];
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) => {
const timestamp = Math.floor(Date.now() / 1e3).toString();
const bodyString = JSON.stringify(requestBodyObject);
const payload = `${timestamp}${bodyString}`;
const encoder = new TextEncoder();
// WARNING: Using potentially hardcoded key found in client-side script
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("");
// WARNING: Using potentially hardcoded API key found in client-side script
return {
"X-API-Key": "62852b00cb9e44bca86f0ec7e7455dc6",
"X-Timestamp": timestamp,
"X-Signature": signatureHex,
"Content-Type": "application/json",
// Add other necessary headers from original init if needed (accept, etc.)
// It's often safer to merge than replace completely if other headers matter
"accept": "*/*",
"accept-language": "en-US,en;q=0.9", // Example, might be needed
};
} catch (error) {
console.error("[Model Selector] Error generating signature:", error);
throw error; // Re-throw to indicate failure
}
};
// --- 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 {
// 1. Parse the original body
let bodyData = JSON.parse(init.body);
// 2. Check if modification is needed
if (bodyData && typeof bodyData === 'object' && bodyData.hasOwnProperty('model') && bodyData.model !== selectedModel) {
console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Original model: ${bodyData.model}`);
// 3. Modify the model in the JS object
bodyData.model = selectedModel;
console.log(`[Model Selector] Modified model to: ${selectedModel}`);
// 4. Regenerate Auth Headers using the MODIFIED body data
console.log('[Model Selector] Regenerating auth headers...');
const newHeaders = await generateAuthHeaders(bodyData);
// 5. Prepare the new init object for fetch
const newInit = {
...init, // Copy original init options (method, credentials, etc.)
headers: newHeaders, // Use the NEW headers
body: JSON.stringify(bodyData) // Use the MODIFIED body string
};
console.log('[Model Selector] Sending fetch with new headers and modified body.');
// 6. Call the ORIGINAL fetch with the NEW init data
return originalFetch(input, newInit);
} else if (bodyData && bodyData.model === selectedModel) {
console.log(`[Model Selector] Intercepted ${API_ENDPOINT_PATH}. Model already matches selection (${selectedModel}). Passing through.`);
} else {
console.warn('[Model Selector] Fetch body did not contain expected "model" property or modification not needed:', bodyData);
}
} catch (e) {
console.error('[Model Selector] Error processing fetch interceptor:', e);
// Fall through to original fetch if error occurs
}
}
// For non-matching requests or if modification wasn't needed/failed, call the original fetch
return originalFetch(input, init);
};
// --- Create Improved UI (No changes needed here) ---
function createEnhancedUI() {
console.log('[Model Selector] createEnhancedUI called');
// ... (Keep the UI code from version 2.1) ...
// Inject Styles
GM_addStyle(`
/* Toggle Button (Top Left Area - Shifted Right) */
#modelSelectorToggleBtn {
position: fixed;
top: 16px; /* Align with header buttons vertically */
left: 60px; /* MOVED right - Adjust value if needed */
z-index: 9998;
background-color: #2a2a2a;
color: #ffffff;
border: 1px solid #c15a17;
border-radius: 50%;
width: 38px;
height: 38px;
font-size: 20px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.2s, transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
#modelSelectorToggleBtn:hover {
background-color: #3a3a3a;
transform: translateY(-1px);
}
/* Main Container Panel (Initially Hidden) */
#modelSelectorContainer {
position: fixed;
top: 65px; /* Position below header */
left: 15px; /* CHANGED FROM right to left */
z-index: 9999;
background-color: #1a1a1a;
border: 1px solid #c15a17;
padding: 0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
font-family: 'Avenir', 'Arial', sans-serif;
color: #ffffff;
min-width: 280px;
display: none;
flex-direction: column;
}
#modelSelectorContainer.visible {
display: flex;
}
/* Panel Header */
#modelSelectorHeader {
background-color: #2a2a2a;
padding: 8px 12px;
border-bottom: 1px solid #c15a17;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 14px;
}
#modelSelectorHeader span {
color: #c15a17;
}
/* Panel Close Button */
#modelSelectorCloseBtn {
background: none;
border: none;
color: #ffffff;
font-size: 22px;
font-weight: bold;
cursor: pointer;
padding: 0 4px;
line-height: 1;
opacity: 0.7;
}
#modelSelectorCloseBtn:hover {
opacity: 1;
}
/* Panel Body */
#modelSelectorBody {
padding: 15px;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Label */
#modelSelectorContainer label {
margin: 0;
font-weight: normal;
color: #dddddd;
font-size: 13px;
}
/* Dropdown */
#modelSelectorDropdown {
width: 100%;
padding: 8px 10px;
background-color: #333333;
color: #ffffff;
border: 1px solid #c15a17;
border-radius: 5px;
font-size: 14px;
box-sizing: border-box;
}
#modelSelectorDropdown option {
background-color: #333333;
color: #ffffff;
padding: 5px 8px;
}
`);
// Create Toggle Button
const toggleBtn = document.createElement('button');
toggleBtn.id = 'modelSelectorToggleBtn';
toggleBtn.innerHTML = '⚙️';
toggleBtn.title = 'Toggle Model Selector';
// Create Panel Container
const container = document.createElement('div');
container.id = 'modelSelectorContainer';
// Create Panel Header
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';
header.appendChild(title);
header.appendChild(closeBtn);
// Create Panel Body
const body = document.createElement('div');
body.id = 'modelSelectorBody';
// Create Label
const label = document.createElement('label');
label.htmlFor = 'modelSelectorDropdown';
label.textContent = 'Active Model:';
// Create Dropdown
const select = document.createElement('select');
select.id = 'modelSelectorDropdown';
// Populate Dropdown
AVAILABLE_MODELS.forEach(model => {
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);
});
// Assemble Panel Body
body.appendChild(label);
body.appendChild(select);
// Assemble Panel Container
container.appendChild(header);
container.appendChild(body);
// Append elements to the actual page body
document.body.appendChild(toggleBtn);
document.body.appendChild(container);
// --- Add 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}`);
toggleBtn.style.borderColor = '#4caf50';
setTimeout(() => { toggleBtn.style.borderColor = '#c15a17'; }, 500);
});
}
// --- Initialize UI ---
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createEnhancedUI);
} else {
createEnhancedUI();
}
})();