Gemini Settings Panel: API version/model selection, parameters, presets, reset, tooltips, export/import, enhanced safety settings, and thinking mode options.
当前为
// ==UserScript==
// @name Chub AI Gemini/PaLM2 Model List Enhancer
// @license MIT
// @namespace http://tampermonkey.net/
// @version 6.3
// @description Gemini Settings Panel: API version/model selection, parameters, presets, reset, tooltips, export/import, enhanced safety settings, and thinking mode options.
// @author Ko16aska
// @match *://chub.ai/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- LocalStorage keys ---
const STORAGE_SETTINGS_KEY = 'chubGeminiSettings';
const STORAGE_PANEL_STATE_KEY = 'chubGeminiPanelState';
const STORAGE_SINGLE_API_KEY = 'chubGeminiApiKey';
const STORAGE_API_KEY_LIST = 'chubGeminiApiKeysList';
// --- Defaults ---
const DEFAULT_MODEL = 'custom';
const DEFAULT_API_VERSION = 'v1beta';
const DEFAULT_USE_CYCLIC_API = false;
const DEFAULT_CURRENT_API_KEY_INDEX = 0;
const DEFAULT_THINKING_BUDGET = -1; // Default: Auto
const DEFAULT_INCLUDE_THOUGHTS = false; // Default: off
const DEFAULT_OVERRIDE_THINKING_BUDGET = false; // Default: off
// --- Safety Settings Options ---
const SAFETY_SETTINGS_OPTIONS = [
{ name: 'BLOCK_NONE', value: 'BLOCK_NONE' },
{ name: 'BLOCK_LOW_AND_ABOVE', value: 'BLOCK_LOW_AND_ABOVE' },
{ name: 'BLOCK_MEDIUM_AND_ABOVE', value: 'BLOCK_MEDIUM_AND_ABOVE' },
{ name: 'BLOCK_HIGH_AND_ABOVE', value: 'BLOCK_HIGH_AND_ABOVE' }
];
const HARM_CATEGORIES = [
'HARM_CATEGORY_HATE_SPEECH',
'HARM_CATEGORY_SEXUALLY_EXPLICIT',
'HARM_CATEGORY_HARASSMENT',
'HARM_CATEGORY_DANGEROUS_CONTENT'
];
// --- State variables ---
let allSettings = {};
let panelState = {
collapsed: true,
currentModel: DEFAULT_MODEL,
currentPreset: null,
apiVersion: DEFAULT_API_VERSION,
useCyclicApi: DEFAULT_USE_CYCLIC_API,
currentApiKeyIndex: DEFAULT_CURRENT_API_KEY_INDEX,
thinkingParamsCollapsed: true
};
let modelList = [];
let apiKeysList = [];
let realApiKey = ''; // The actual API key used for requests
// --- Create panel ---
function createPanel() {
const panel = document.createElement('div');
panel.id = 'gemini-settings-panel';
if (panelState.collapsed) panel.classList.add('collapsed');
panel.innerHTML = `
<div class="toggle-button" title="Show/Hide Panel">▶</div>
<div class="panel-content">
<h4>Gemini Settings</h4>
<label>API Key:
<input type="password" id="api-key-input" autocomplete="off" placeholder="Insert API key here" />
<button id="btn-manage-api-keys">Manage Keys</button>
</label>
<label class="toggle-switch-label">
<input type="checkbox" id="toggle-cyclic-api" />
<span class="slider round"></span>
Use API cyclically for each request
</label>
<div class="param-group">
<label>
API Version:
<div class="input-container">
<select id="apiVersion-select">
<option value="v1beta">v1beta</option>
<option value="v1">v1</option>
</select>
<span class="tooltip" title="v1beta: Contains new features, but may be unstable. v1: Stable, recommended for production use.">?</span>
</div>
</label>
</div>
<button id="btn-get-models">Get Models List</button>
<label>Preset:
<select id="preset-select"></select>
<button id="btn-add-preset">Add</button>
<button id="btn-delete-preset">Delete</button>
</label>
<!-- Corrected Model Selection Group -->
<div class="param-group">
<label>Model:</label>
<div class="input-container model-input-container">
<select id="model-select"></select>
<input type="text" id="custom-model-input" placeholder="Enter your model" style="display:none;" />
<button id="btn-toggle-thinking-params" title="Toggle Thinking Mode Options">🧠</button>
</div>
</div>
<!-- New section for Thinking Mode Parameters, initially hidden -->
<div id="thinking-mode-params" style="display:none;">
<!-- Override Thinking Budget Toggle -->
<label class="toggle-switch-label">
<input type="checkbox" id="toggle-overrideThinkingBudget" />
<span class="slider round"></span>
Override Thinking Budget
<span class="tooltip" title="The thinking budget parameter works with Gemini 2.5 Pro, 2.5 Flash, and 2.5 Flash Lite.">?</span>
</label>
<!-- thinkingBudget -->
<div class="param-group" id="thinking-budget-group">
<label>
Thinking Budget:
<div class="input-container">
<input type="number" step="1" id="param-thinkingBudget" />
<span class="tooltip" title="Controls the computational budget for thinking. -1 for dynamic, 0 to disable (default), or a specific token count (up to 24576 (32768 for Gemini 2.5 pro)).">?</span>
</div>
</label>
<input type="range" id="range-thinkingBudget" min="-1" max="32768" step="1" />
</div>
<!-- includeThoughts -->
<label class="toggle-switch-label" id="include-thoughts-label">
<input type="checkbox" id="toggle-includeThoughts" />
<span class="slider round"></span>
Include Thoughts in Response
<span class="tooltip" title="If enabled, the model's internal thought process will be included in the response.">?</span>
</label>
</div>
<!-- Temperature -->
<div class="param-group">
<label>
Temperature:
<div class="input-container">
<input type="number" step="0.01" id="param-temperature" />
<span class="tooltip" title="Controls the randomness of the output. Higher values make the output more random, while lower values make it more deterministic.">?</span>
</div>
</label>
<input type="range" id="range-temperature" min="0" max="2" step="0.01" />
</div>
<!-- Max Output Tokens -->
<div class="param-group">
<label>
Max Output Tokens:
<div class="input-container">
<input type="number" step="1" id="param-maxTokens" />
<span class="tooltip" title="The maximum number of tokens to generate.">?</span>
</div>
</label>
<input type="range" id="range-maxTokens" min="1" max="65536" step="1" />
</div>
<!-- topP -->
<div class="param-group">
<label>
Top P:
<div class="input-container">
<input type="number" step="0.01" id="param-topP" />
<span class="tooltip" title="Nucleus sampling parameter. The model considers the smallest set of tokens whose cumulative probability exceeds topP.">?</span>
</div>
</label>
<input type="range" id="range-topP" min="0" max="1" step="0.01" />
</div>
<!-- topK -->
<div class="param-group">
<label>
Top K:
<div class="input-container">
<input type="number" step="1" id="param-topK" />
<span class="tooltip" title="The model considers only the K tokens with the highest probability.">?</span>
</div>
</label>
<input type="range" id="range-topK" min="0" max="1000" step="1" />
</div>
<!-- candidateCount -->
<div class="param-group">
<label>
Candidate Count:
<div class="input-container">
<input type="number" step="1" id="param-candidateCount" />
<span class="tooltip" title="The number of generated responses to return. Must be 1.">?</span>
</div>
</label>
<input type="range" id="range-candidateCount" min="1" max="8" step="1" />
</div>
<!-- frequencyPenalty -->
<div class="param-group">
<label>
Frequency Penalty:
<div class="input-container">
<input type="number" step="0.01" id="param-frequencyPenalty" />
<span class="tooltip" title="Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.">?</span>
</div>
</label>
<input type="range" id="range-frequencyPenalty" min="-2.0" max="2.0" step="0.01" />
</div>
<!-- presencePenalty -->
<div class="param-group">
<label>
Presence Penalty:
<div class="input-container">
<input type="number" step="0.01" id="param-presencePenalty" />
<span class="tooltip" title="Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.">?</span>
</div>
</label>
<input type="range" id="range-presencePenalty" min="-2.0" max="2.0" step="0.01" />
</div>
<!-- safetySettings -->
<div class="param-group">
<label>
Safety Settings:
<div class="input-container">
<select id="safety-settings-select"></select>
<span class="tooltip" title="Adjusts the safety filtering threshold for generated content.">?</span>
</div>
</label>
</div>
<button id="btn-save-settings">Save Settings</button>
<button id="btn-reset-settings">Reset to Defaults</button>
<button id="btn-export-settings">Export Settings</button>
<button id="btn-import-settings">Import Settings</button>
<input type="file" id="input-import-settings" style="display:none;" accept=".json" />
<div id="save-toast">Settings saved!</div>
</div>
`;
document.body.appendChild(panel);
const apiKeyListModal = document.createElement('div');
apiKeyListModal.id = 'api-key-list-modal';
apiKeyListModal.style.display = 'none'; // Hidden by default
apiKeyListModal.innerHTML = `
<div class="modal-content">
<h4>Manage API Keys</h4>
<textarea id="api-keys-textarea" placeholder="Enter API keys, one per line"></textarea>
<div class="modal-buttons">
<button id="btn-save-api-keys">Save</button>
<button id="btn-cancel-api-keys">Cancel</button>
</div>
</div>
`;
document.body.appendChild(apiKeyListModal);
const style = document.createElement('style');
style.textContent = `
:root {
--scale-factor: 1.0;
}
#gemini-settings-panel {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%) translateX(100%);
background: rgba(30,30,30,0.85);
color: #eee;
border-left: calc(1px * var(--scale-factor)) solid #444;
border-radius: calc(8px * var(--scale-factor)) 0 0 calc(8px * var(--scale-factor));
padding: 0; /* MODIFIED: Padding moved to inner container */
box-shadow: 0 calc(4px * var(--scale-factor)) calc(16px * var(--scale-factor)) rgba(0,0,0,0.7);
font-family: Arial, sans-serif;
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
z-index: 10000;
transition: transform 0.4s ease;
user-select: none;
width: max-content;
max-width: calc(min(80vw, 350px) * var(--scale-factor));
box-sizing: border-box;
max-height: 90vh; /* MODIFIED: Height constraint for the whole panel */
display: flex; /* MODIFIED: To help size the inner container */
}
#gemini-settings-panel:not(.collapsed) {
transform: translateY(-50%) translateX(0);
}
#gemini-settings-panel h4 {
text-align: center;
margin: 0 0 calc(min(1.2vw, 5px) * var(--scale-factor));
font-size: calc(min(3vw, 16px) * var(--scale-factor));
}
#gemini-settings-panel label {
display: block;
margin-bottom: calc(min(0.8vw, 3px) * var(--scale-factor));
font-weight: 600;
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
}
#gemini-settings-panel input[type="number"],
#gemini-settings-panel input[type="text"],
#gemini-settings-panel input[type="password"],
#gemini-settings-panel select {
background: #222;
border: calc(1px * var(--scale-factor)) solid #555;
border-radius: calc(4px * var(--scale-factor));
color: #eee;
padding: calc(min(0.4vw, 2px) * var(--scale-factor)) calc(min(0.8vw, 4px) * var(--scale-factor));
font-size: calc(min(2.3vw, 13px) * var(--scale-factor));
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* Styling for API Key input and button side-by-side */
#gemini-settings-panel label:has(#api-key-input) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: calc(min(0.8vw, 4px) * var(--scale-factor));
}
#gemini-settings-panel label:has(#api-key-input) #api-key-input {
flex-grow: 1;
min-width: calc(100px * var(--scale-factor));
}
#gemini-settings-panel label:has(#api-key-input) #btn-manage-api-keys {
width: auto;
padding: calc(min(0.6vw, 3px) * var(--scale-factor)) calc(min(1vw, 6px) * var(--scale-factor));
margin-top: 0;
}
/* Styling for the Model input container */
.model-input-container #model-select,
.model-input-container #custom-model-input {
flex-grow: 1; /* Allows the select/input to fill available space */
min-width: 0; /* Critical for flex-grow to work properly */
}
.model-input-container #btn-toggle-thinking-params {
flex-shrink: 0; /* Prevents the button from shrinking */
width: auto;
margin: 0; /* Reset margins from general button rules */
padding: calc(min(0.6vw, 3px) * var(--scale-factor)) calc(min(1vw, 6px) * var(--scale-factor));
line-height: 1; /* Better emoji alignment */
font-size: calc(min(3vw, 16px) * var(--scale-factor));
}
.param-group {
margin-bottom: calc(min(1.2vw, 5px) * var(--scale-factor));
}
.param-group label {
display: block;
margin-bottom: calc(min(0.5vw, 1px) * var(--scale-factor));
font-weight: 600;
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
}
.param-group .input-container {
display: flex;
align-items: center;
gap: calc(min(0.8vw, 3px) * var(--scale-factor));
margin-top: calc(0.2vw * var(--scale-factor));
}
.param-group .input-container input[type="number"],
.param-group .input-container input[type="text"],
.param-group .input-container input[type="password"],
.param-group .input-container select {
flex-grow: 1;
min-width: 0;
background: #222;
border: calc(1px * var(--scale-factor)) solid #555;
border-radius: calc(4px * var(--scale-factor));
color: #eee;
padding: calc(min(0.4vw, 2px) * var(--scale-factor)) calc(min(0.8vw, 4px) * var(--scale-factor));
font-size: calc(min(2.3vw, 13px) * var(--scale-factor));
box-sizing: border-box;
}
.tooltip {
flex: 0 0 auto;
cursor: help;
color: #aaa;
font-size: calc(min(2vw, 12px) * var(--scale-factor));
user-select: none;
}
.param-group input[type="range"] {
width: 100% !important;
margin-top: calc(min(0.8vw, 2px) * var(--scale-factor));
cursor: pointer;
display: block;
height: calc(4px * var(--scale-factor));
-webkit-appearance: none;
background: #555;
border-radius: calc(2px * var(--scale-factor));
}
.param-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: calc(12px * var(--scale-factor));
height: calc(12px * var(--scale-factor));
background: #4caf50;
border-radius: 50%;
cursor: pointer;
}
.param-group input[type="range"]::-moz-range-thumb {
width: calc(12px * var(--scale-factor));
height: calc(12px * var(--scale-factor));
background: #4caf50;
border-radius: 50%;
cursor: pointer;
}
#btn-save-settings, #btn-get-models, #btn-add-preset, #btn-delete-preset, #btn-reset-settings, #btn-export-settings, #btn-import-settings, #btn-manage-api-keys, #btn-save-api-keys, #btn-cancel-api-keys, #btn-toggle-thinking-params {
width: 100%;
padding: calc(min(0.8vw, 4px) * var(--scale-factor));
border: none;
border-radius: calc(5px * var(--scale-factor));
background: #4caf50;
color: #fff;
font-weight: 600;
cursor: pointer;
user-select: none;
margin-top: calc(min(0.6vw, 3px) * var(--scale-factor));
transition: background-color 0.3s ease;
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
}
#btn-get-models {
margin-bottom: calc(min(1.2vw, 5px) * var(--scale-factor));
}
/* New section for thinking mode parameters */
#thinking-mode-params {
border: calc(1px * var(--scale-factor)) solid #555;
border-radius: calc(5px * var(--scale-factor));
padding: calc(min(1vw, 5px) * var(--scale-factor));
margin-top: calc(min(1vw, 5px) * var(--scale-factor));
margin-bottom: calc(min(1.2vw, 5px) * var(--scale-factor));
background: rgba(40, 40, 40, 0.7);
}
#thinking-mode-params .param-group:last-child {
margin-bottom: 0;
}
#thinking-mode-params .toggle-switch-label {
margin-bottom: calc(min(0.8vw, 3px) * var(--scale-factor));
}
#thinking-mode-params .toggle-switch-label:last-of-type {
margin-bottom: 0;
}
/* Style for disabled thinking controls */
.param-group.disabled, .toggle-switch-label.disabled {
opacity: 0.5;
pointer-events: none;
}
#btn-save-settings:hover, #btn-get-models:hover, #btn-add-preset:hover, #btn-delete-preset:hover, #btn-reset-settings:hover, #btn-export-settings:hover, #btn-import-settings:hover, #btn-manage-api-keys:hover, #btn-cancel-api-keys:hover, #btn-save-api-keys:hover, #btn-toggle-thinking-params:hover {
background: #388e3c;
}
#save-toast {
margin-top: calc(min(1.5vw, 4px) * var(--scale-factor));
text-align: center;
background: #222;
color: #0f0;
padding: calc(min(0.8vw, 4px) * var(--scale-factor));
border-radius: calc(5px * var(--scale-factor));
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
user-select: none;
font-size: calc(min(2.3vw, 13px) * var(--scale-factor));
}
#save-toast.show {
opacity: 1;
}
.toggle-button {
position: absolute !important;
left: calc(-28px * var(--scale-factor)) !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: calc(28px * (var(--scale-factor))) !important;
height: calc(48px * (var(--scale-factor))) !important;
background: rgba(30,30,30,0.85) !important;
border: calc(1px * var(--scale-factor)) solid #444 !important;
border-radius: calc(8px * var(--scale-factor)) 0 0 calc(8px * var(--scale-factor)) !important;
color: #eee !important;
text-align: center !important;
line-height: calc(48px * var(--scale-factor)) !important;
font-size: calc(min(4vw, 20px) * var(--scale-factor)) !important;
cursor: pointer !important;
user-select: none !important;
transition: transform 0.3s ease !important;
}
#gemini-settings-panel.collapsed .toggle-button {
transform: translateY(-50%) rotate(0deg);
}
#gemini-settings-panel:not(.collapsed) .toggle-button {
transform: translateY(-50%) rotate(0deg);
}
/* Toggle Switch CSS for "Use API cyclically for each request" and "Include Thoughts"*/
.toggle-switch-label {
position: relative;
display: flex;
align-items: center;
width: 100%;
margin-top: calc(min(0.8vw, 3px) * var(--scale-factor));
margin-bottom: calc(min(0.8vw, 3px) * var(--scale-factor));
font-size: calc(min(2.2vw, 12px) * var(--scale-factor));
padding-left: calc(min(6vw, 35px) * var(--scale-factor));
cursor: pointer;
user-select: none;
box-sizing: border-box;
min-height: calc(min(3vw, 18px) * var(--scale-factor));
transition: opacity 0.3s ease;
}
.toggle-switch-label input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
height: calc(min(3vw, 18px) * var(--scale-factor));
width: calc(min(5.5vw, 32px) * var(--scale-factor));
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
border-radius: calc(min(1.5vw, 9px) * var(--scale-factor));
}
.slider:before {
position: absolute;
content: "";
height: calc(min(2.2vw, 13px) * var(--scale-factor));
width: calc(min(2.2vw, 13px) * var(--scale-factor));
left: calc(min(0.4vw, 2.5px) * var(--scale-factor));
bottom: calc(min(0.4vw, 2.5px) * var(--scale-factor));
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4caf50;
}
input:focus + .slider {
box-shadow: 0 0 calc(min(0.5vw, 1px) * var(--scale-factor)) #4caf50;
}
input:checked + .slider:before {
-webkit-transform: translateX(calc(min(2.5vw, 14px) * var(--scale-factor)));
-ms-transform: translateX(calc(min(2.5vw, 14px) * var(--scale-factor)));
transform: translateX(calc(min(2.5vw, 14px) * var(--scale-factor)));
}
/* Responsive adjustments for toggle switch */
@media screen and (max-width: 600px) {
.toggle-switch-label {
min-height: calc(min(4vw, 18px) * var(--scale-factor));
padding-left: calc(min(8vw, 35px) * var(--scale-factor));
}
.slider {
height: calc(min(4vw, 18px) * var(--scale-factor));
width: calc(min(7vw, 32px) * var(--scale-factor));
}
.slider:before {
height: calc(min(3vw, 13px) * var(--scale-factor));
width: calc(min(3vw, 13px) * var(--scale-factor));
}
input:checked + .slider:before {
-webkit-transform: translateX(calc(min(4vw, 14px) * var(--scale-factor)));
-ms-transform: translateX(calc(min(4vw, 14px) * var(--scale-factor)));
transform: translateX(calc(min(4vw, 14px) * var(--scale-factor)));
}
}
/* API Key List Modal Styles */
#api-key-list-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7); /* Dimmed background */
display: flex; /* Use flexbox for centering */
justify-content: center;
align-items: center;
z-index: 10001; /* Must be on top of the settings panel */
}
#api-key-list-modal .modal-content {
background: #333;
padding: calc(min(2vw, 15px) * var(--scale-factor));
border-radius: calc(8px * var(--scale-factor));
box-shadow: 0 calc(4px * var(--scale-factor)) calc(20px * var(--scale-factor)) rgba(0,0,0,0.9);
width: calc(min(90vw, 500px) * var(--scale-factor));
max-height: calc(90vh * var(--scale-factor));
display: flex;
flex-direction: column;
gap: calc(min(1.5vw, 10px) * var(--scale-factor));
}
#api-key-list-modal h4 {
color: #eee;
text-align: center;
margin: 0;
font-size: calc(min(3.5vw, 18px) * var(--scale-factor));
}
#api-key-list-modal textarea {
width: 100%;
flex-grow: 1; /* Allows the textarea to take up available space */
min-height: calc(150px * var(--scale-factor)); /* Minimum height */
background: #222;
border: calc(1px * var(--scale-factor)) solid #555;
border-radius: calc(4px * var(--scale-factor));
color: #eee;
padding: calc(min(1vw, 5px) * var(--scale-factor));
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
resize: vertical; /* Allow vertical resizing only */
box-sizing: border-box;
}
#api-key-list-modal .modal-buttons {
display: flex;
justify-content: flex-end; /* Buttons on the right */
gap: calc(min(1vw, 8px) * var(--scale-factor));
margin-top: calc(min(1vw, 8px) * var(--scale-factor));
}
#api-key-list-modal .modal-buttons button {
padding: calc(min(0.8vw, 6px) * var(--scale-factor)) calc(min(1.5vw, 12px) * var(--scale-factor));
font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
width: auto; /* Remove 100% width */
}
#api-key-list-modal #btn-save-api-keys {
background: #4caf50;
}
#api-key-list-modal #btn-save-api-keys:hover {
background: #388e3c;
}
#api-key-list-modal #btn-cancel-api-keys {
background: #f44336; /* Red for cancel */
}
#api-key-list-modal #btn-cancel-api-keys:hover {
background: #d32f2f;
}
/* MODIFIED: Styles moved from panel to inner content div */
.panel-content {
flex: 1;
min-height: 0;
padding: calc(min(1.2vw, 6px) * var(--scale-factor)) calc(min(2vw, 10px) * var(--scale-factor));
box-sizing: border-box;
overflow-y: auto; /* Enables vertical scrolling */
scrollbar-width: thin; /* For Firefox */
scrollbar-color: #888 #333; /* For Firefox */
}
.panel-content::-webkit-scrollbar {
width: 8px; /* Width of the scrollbar */
}
.panel-content::-webkit-scrollbar-track {
background: #333; /* Background of the scrollbar track */
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb {
background-color: #888; /* Color of the scrollbar thumb */
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background-color: #aaa; /* Color of the scrollbar thumb on hover */
}
`;
document.head.appendChild(style);
let lastDevicePixelRatio = window.devicePixelRatio;
function updateScaleFactor() {
const scale = 1 / window.devicePixelRatio;
document.documentElement.style.setProperty('--scale-factor', scale);
}
function checkZoom() {
if (window.devicePixelRatio !== lastDevicePixelRatio) {
lastDevicePixelRatio = window.devicePixelRatio;
updateScaleFactor();
}
requestAnimationFrame(checkZoom);
}
updateScaleFactor();
checkZoom();
const toggleBtn = panel.querySelector('.toggle-button');
const apiKeyInput = panel.querySelector('#api-key-input');
const btnManageApiKeys = panel.querySelector('#btn-manage-api-keys');
const toggleCyclicApi = panel.querySelector('#toggle-cyclic-api');
const apiVersionSelect = panel.querySelector('#apiVersion-select');
const btnGetModels = panel.querySelector('#btn-get-models');
const presetSelect = panel.querySelector('#preset-select');
const btnAddPreset = panel.querySelector('#btn-add-preset');
const btnDeletePreset = panel.querySelector('#btn-delete-preset');
const modelSelect = panel.querySelector('#model-select');
const customModelInput = panel.querySelector('#custom-model-input');
const btnToggleThinkingParams = panel.querySelector('#btn-toggle-thinking-params');
const thinkingModeParamsDiv = panel.querySelector('#thinking-mode-params');
const toggleOverrideThinkingBudget = panel.querySelector('#toggle-overrideThinkingBudget');
const thinkingBudgetGroup = panel.querySelector('#thinking-budget-group');
const includeThoughtsLabel = panel.querySelector('#include-thoughts-label');
const toggleIncludeThoughts = panel.querySelector('#toggle-includeThoughts');
const btnSaveSettings = panel.querySelector('#btn-save-settings');
const btnResetSettings = panel.querySelector('#btn-reset-settings');
const btnExportSettings = panel.querySelector('#btn-export-settings');
const inputImportSettings = panel.querySelector('#input-import-settings');
const btnImportSettings = panel.querySelector('#btn-import-settings');
const saveToast = panel.querySelector('#save-toast');
const safetySettingsSelect = panel.querySelector('#safety-settings-select');
// Modal elements
const apiKeyKeysTextarea = apiKeyListModal.querySelector('#api-keys-textarea');
const btnSaveApiKeys = apiKeyListModal.querySelector('#btn-save-api-keys');
const btnCancelApiKeys = apiKeyListModal.querySelector('#btn-cancel-api-keys');
const elems = {
temperature: { num: panel.querySelector('#param-temperature'), range: panel.querySelector('#range-temperature') },
maxTokens: { num: panel.querySelector('#param-maxTokens'), range: panel.querySelector('#range-maxTokens') },
topP: { num: panel.querySelector('#param-topP'), range: panel.querySelector('#range-topP') },
topK: { num: panel.querySelector('#param-topK'), range: panel.querySelector('#range-topK') },
candidateCount: { num: panel.querySelector('#param-candidateCount'), range: panel.querySelector('#range-candidateCount') },
frequencyPenalty: { num: panel.querySelector('#param-frequencyPenalty'), range: panel.querySelector('#range-frequencyPenalty') },
presencePenalty: { num: panel.querySelector('#param-presencePenalty'), range: panel.querySelector('#range-presencePenalty') },
thinkingBudget: { num: panel.querySelector('#param-thinkingBudget'), range: panel.querySelector('#range-thinkingBudget') }
};
// Populate safety settings select
SAFETY_SETTINGS_OPTIONS.forEach(option => {
const optElem = document.createElement('option');
optElem.value = option.value;
optElem.textContent = option.name;
safetySettingsSelect.appendChild(optElem);
});
// Click outside panel to collapse
document.addEventListener('click', (event) => {
if (!panel.contains(event.target) && !toggleBtn.contains(event.target) && !apiKeyListModal.contains(event.target) && !panelState.collapsed) {
panelState.collapsed = true;
panel.classList.add('collapsed');
saveAllSettings();
}
});
function getApiUrl(path) {
return `https://generativelanguage.googleapis.com/${panelState.apiVersion}/${path}`;
}
function maskKeyDisplay(key) {
if (!key) return '';
if (key.length <= 4) return '*'.repeat(key.length);
return key[0] + '*'.repeat(key.length - 2) + key[key.length - 1];
}
function loadGlobalApiKeySettings() {
const storedKeysList = localStorage.getItem(STORAGE_API_KEY_LIST) || '';
apiKeysList = storedKeysList.split('\n').map(k => k.trim()).filter(k => k.length > 0);
if (panelState.currentApiKeyIndex === undefined || panelState.currentApiKeyIndex < 0 || panelState.currentApiKeyIndex >= apiKeysList.length) {
panelState.currentApiKeyIndex = 0;
}
updateRealApiKey();
}
function updateRealApiKey() {
if (panelState.useCyclicApi && apiKeysList.length > 0) {
realApiKey = apiKeysList[panelState.currentApiKeyIndex % apiKeysList.length];
apiKeyInput.disabled = true;
apiKeyInput.type = 'text';
apiKeyInput.value = realApiKey;
apiKeyInput.setAttribute('title', 'Currently active key from your list (disabled in cyclic mode). Use "Manage Keys" to edit the list.');
} else {
realApiKey = localStorage.getItem(STORAGE_SINGLE_API_KEY) || '';
apiKeyInput.disabled = false;
apiKeyInput.type = 'password';
apiKeyInput.value = maskKeyDisplay(realApiKey);
apiKeyInput.removeAttribute('title');
}
toggleCyclicApi.checked = panelState.useCyclicApi;
}
function saveApiKeysListFromModal(keysString) {
localStorage.setItem(STORAGE_API_KEY_LIST, keysString);
apiKeysList = keysString.split('\n').map(k => k.trim()).filter(k => k.length > 0);
if (panelState.currentApiKeyIndex >= apiKeysList.length) {
panelState.currentApiKeyIndex = 0;
}
saveAllSettings();
updateRealApiKey();
apiKeyListModal.style.display = 'none';
}
function saveSingleApiKey(newKey) {
localStorage.setItem(STORAGE_SINGLE_API_KEY, newKey.trim());
if (!panelState.useCyclicApi) {
realApiKey = newKey.trim();
}
updateRealApiKey();
}
apiKeyInput.addEventListener('focus', () => {
if (!panelState.useCyclicApi) {
apiKeyInput.type = 'text';
apiKeyInput.value = realApiKey;
}
});
apiKeyInput.addEventListener('blur', () => {
if (!panelState.useCyclicApi) {
saveSingleApiKey(apiKeyInput.value);
apiKeyInput.type = 'password';
apiKeyInput.value = maskKeyDisplay(realApiKey);
}
});
btnManageApiKeys.addEventListener('click', () => {
apiKeyKeysTextarea.value = apiKeysList.join('\n');
apiKeyListModal.style.display = 'flex';
});
btnSaveApiKeys.addEventListener('click', () => {
saveApiKeysListFromModal(apiKeyKeysTextarea.value);
});
btnCancelApiKeys.addEventListener('click', () => {
apiKeyListModal.style.display = 'none';
});
toggleCyclicApi.addEventListener('change', () => {
panelState.useCyclicApi = toggleCyclicApi.checked;
if (panelState.useCyclicApi && apiKeysList.length === 0) {
alert('No API keys found in the list. Please add keys using "Manage Keys" to use cyclic mode.');
panelState.useCyclicApi = false;
toggleCyclicApi.checked = false;
}
saveAllSettings();
updateRealApiKey();
});
apiVersionSelect.addEventListener('change', () => {
panelState.apiVersion = apiVersionSelect.value;
saveAllSettings();
});
function fillModelSelect() {
modelSelect.innerHTML = '';
const optCustom = document.createElement('option');
optCustom.value = 'custom';
optCustom.textContent = 'Custom';
modelSelect.appendChild(optCustom);
for (const m of modelList) {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
modelSelect.appendChild(opt);
}
}
function updateCustomModelInputVisibility() {
if(modelSelect.value === 'custom') {
customModelInput.style.display = 'block';
} else {
customModelInput.style.display = 'none';
}
}
function updateThinkingParamsVisibility() {
if (panelState.thinkingParamsCollapsed) {
thinkingModeParamsDiv.style.display = 'none';
} else {
thinkingModeParamsDiv.style.display = 'block';
}
}
function updateThinkingControlsState() {
const isEnabled = toggleOverrideThinkingBudget.checked;
thinkingBudgetGroup.classList.toggle('disabled', !isEnabled);
includeThoughtsLabel.classList.toggle('disabled', !isEnabled);
elems.thinkingBudget.num.disabled = !isEnabled;
elems.thinkingBudget.range.disabled = !isEnabled;
toggleIncludeThoughts.disabled = !isEnabled;
}
toggleOverrideThinkingBudget.addEventListener('change', updateThinkingControlsState);
function loadModelSettings(model) {
if (!model) model = DEFAULT_MODEL;
const settings = allSettings[model] || {
temperature: 2.0,
maxOutputTokens: 65536,
topP: 0.95,
topK: 0,
candidateCount: 1,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
safetySettingsThreshold: 'BLOCK_NONE',
thinkingBudget: DEFAULT_THINKING_BUDGET,
includeThoughts: DEFAULT_INCLUDE_THOUGHTS,
overrideThinkingBudget: DEFAULT_OVERRIDE_THINKING_BUDGET
};
elems.temperature.num.value = settings.temperature;
elems.temperature.range.value = settings.temperature;
elems.maxTokens.num.value = settings.maxOutputTokens;
elems.maxTokens.range.value = settings.maxOutputTokens;
elems.topP.num.value = settings.topP;
elems.topP.range.value = settings.topP;
elems.topK.num.value = settings.topK;
elems.topK.range.value = settings.topK;
elems.candidateCount.num.value = settings.candidateCount;
elems.candidateCount.range.value = settings.candidateCount;
elems.frequencyPenalty.num.value = settings.frequencyPenalty;
elems.frequencyPenalty.range.value = settings.frequencyPenalty;
elems.presencePenalty.num.value = settings.presencePenalty;
elems.presencePenalty.range.value = settings.presencePenalty;
safetySettingsSelect.value = settings.safetySettingsThreshold;
elems.thinkingBudget.num.value = settings.thinkingBudget;
elems.thinkingBudget.range.value = settings.thinkingBudget;
toggleIncludeThoughts.checked = settings.includeThoughts;
toggleOverrideThinkingBudget.checked = settings.overrideThinkingBudget;
if (model === 'custom') {
customModelInput.value = allSettings.customModelString || '';
} else {
customModelInput.value = '';
}
updateThinkingControlsState();
}
function getCurrentSettings() {
return {
temperature: clamp(parseFloat(elems.temperature.num.value), 0, 2),
maxOutputTokens: clamp(parseInt(elems.maxTokens.num.value), 1, 65536),
topP: clamp(parseFloat(elems.topP.num.value), 0, 1),
topK: clamp(parseInt(elems.topK.num.value), 0, 1000),
candidateCount: clamp(parseInt(elems.candidateCount.num.value), 1, 8),
frequencyPenalty: clamp(parseFloat(elems.frequencyPenalty.num.value), -2.0, 2.0),
presencePenalty: clamp(parseFloat(elems.presencePenalty.num.value), -2.0, 2.0),
safetySettingsThreshold: safetySettingsSelect.value,
customModelString: customModelInput.value.trim(),
thinkingBudget: clamp(parseInt(elems.thinkingBudget.num.value), -1, 32768),
includeThoughts: toggleIncludeThoughts.checked,
overrideThinkingBudget: toggleOverrideThinkingBudget.checked
};
}
function saveModelSettings(model) {
if (!model) model = DEFAULT_MODEL;
const settings = getCurrentSettings();
allSettings[model] = {
temperature: settings.temperature,
maxOutputTokens: settings.maxOutputTokens,
topP: settings.topP,
topK: settings.topK,
candidateCount: settings.candidateCount,
frequencyPenalty: settings.frequencyPenalty,
presencePenalty: settings.presencePenalty,
safetySettingsThreshold: settings.safetySettingsThreshold,
thinkingBudget: settings.thinkingBudget,
includeThoughts: settings.includeThoughts,
overrideThinkingBudget: settings.overrideThinkingBudget
};
if (model === 'custom') {
allSettings.customModelString = settings.customModelString;
}
if (panelState.currentPreset) {
const preset = allSettings.presets.find(p => p.name === panelState.currentPreset);
if (preset) {
preset.model = model;
preset.settings = settings;
}
}
saveAllSettings();
}
function clamp(val, min, max) {
if (isNaN(val)) return min;
return Math.min(max, Math.max(min, val));
}
function linkInputs(numInput, rangeInput, min, max, step) {
numInput.min = min;
numInput.max = max;
numInput.step = step;
rangeInput.min = min;
rangeInput.max = max;
rangeInput.step = step;
numInput.addEventListener('input', () => {
let v = clamp(parseFloat(numInput.value), min, max);
numInput.value = v;
rangeInput.value = v;
});
rangeInput.addEventListener('input', () => {
let v = clamp(parseFloat(rangeInput.value), min, max);
rangeInput.value = v;
numInput.value = v;
});
}
function saveAllSettings() {
try {
localStorage.setItem(STORAGE_SETTINGS_KEY, JSON.stringify(allSettings));
localStorage.setItem(STORAGE_PANEL_STATE_KEY, JSON.stringify(panelState));
showSaveToast();
} catch(e) {
console.error('Error saving settings:', e);
}
}
function loadAllSettings() {
try {
const s = localStorage.getItem(STORAGE_SETTINGS_KEY);
if (s) allSettings = JSON.parse(s);
else allSettings = {};
} catch(e) {
console.error('Error loading settings:', e);
allSettings = {};
}
if(allSettings.modelList && Array.isArray(allSettings.modelList)) {
modelList = allSettings.modelList;
} else {
modelList = [];
}
}
function loadPanelState() {
try {
const s = localStorage.getItem(STORAGE_PANEL_STATE_KEY);
if(s) {
const loadedState = JSON.parse(s);
panelState = {
collapsed: true,
currentModel: DEFAULT_MODEL,
currentPreset: null,
apiVersion: DEFAULT_API_VERSION,
useCyclicApi: DEFAULT_USE_CYCLIC_API,
currentApiKeyIndex: DEFAULT_CURRENT_API_KEY_INDEX,
thinkingParamsCollapsed: true,
...loadedState
};
}
} catch(e) {
console.error('Error loading panel state:', e);
}
}
let toastTimeout = null;
function showSaveToast() {
saveToast.classList.add('show');
if(toastTimeout) clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => {
saveToast.classList.remove('show');
}, 1800);
}
async function fetchModelsFromApi() {
const keyToUse = realApiKey;
if(!keyToUse) {
alert('Please enter an API key or add keys to the list.');
return;
}
btnGetModels.disabled = true;
btnGetModels.textContent = 'Loading...';
try {
const url = getApiUrl(`models?key=${encodeURIComponent(keyToUse)}`);
const response = await fetch(url);
if(!response.ok) throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`);
const data = await response.json();
if(data.models && Array.isArray(data.models)) {
modelList = data.models
.map(m => m.name)
.map(name => name.startsWith('models/') ? name.substring('models/'.length) : name);
} else {
modelList = [];
}
fillModelSelect();
allSettings.modelList = modelList;
saveAllSettings();
if(panelState.currentModel && modelList.includes(panelState.currentModel)) {
modelSelect.value = panelState.currentModel;
} else {
modelSelect.value = DEFAULT_MODEL;
panelState.currentModel = DEFAULT_MODEL;
}
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
} catch (e) {
alert('Error loading models: ' + e.message);
console.error(e);
} finally {
btnGetModels.disabled = false;
btnGetModels.textContent = 'Get Models List';
}
}
function replaceUrlParts(url, modelName) {
if(typeof url !== 'string') return url;
url = url.replace(/(v1beta|v1)\//, `${panelState.apiVersion}/`);
if(modelName === 'custom') {
modelName = allSettings.customModelString || '';
if(!modelName) return url;
}
return url.replace(/(models\/[^:]+)(:)?/, (m, p1, p2) => {
const newModelPath = 'models/' + modelName;
return newModelPath + (p2 || '');
});
}
const originalFetch = window.fetch; // Store the original fetch function
window.fetch = async function(input, init) {
let requestUrl;
let requestInit = init || {};
// Determine the request URL and consolidate init properties if 'input' is a Request object
if (input instanceof Request) {
requestUrl = input.url;
const { url, ...inputProps } = input; // Destructure to get other Request properties
requestInit = { ...inputProps, ...requestInit }; // Merge Request properties with provided init
delete requestInit.url; // URL is handled by requestUrl variable
} else if (typeof input === 'string') {
requestUrl = input;
} else {
// If the input is neither a string nor a Request object, it's an unsupported format for our
// modification. Pass it directly to the original fetch without interception.
return originalFetch(input, init);
}
// --- Determine if this is a Google Generative Language API call ---
// This is the CRITICAL change: only proceed with modifications if it's the target API.
const isGoogleGenLangApi = requestUrl.includes('generativelanguage.googleapis.com');
// If it's NOT a Google API request, immediately pass it to the original fetch.
// This prevents interfering with other website resources (like the tokenizer.json).
if (!isGoogleGenLangApi) {
return originalFetch(input, init);
}
// --- From this point onwards, all logic applies ONLY to Google Generative Language API requests ---
let currentUrlObj;
try {
currentUrlObj = new URL(requestUrl);
} catch (e) {
// If the URL is invalid even for a Google API request, log a warning and use the original fetch.
console.warn('Invalid URL encountered for Google API, skipping modification:', requestUrl, e);
return originalFetch(input, init);
}
// --- API Key Management Logic ---
const isGenerateContentRequest = requestUrl.includes('generateContent?');
if (panelState.useCyclicApi) {
// If cyclic API use is enabled, apply the next key from the list
if (isGenerateContentRequest && apiKeysList.length > 0) {
const nextApiKey = apiKeysList[panelState.currentApiKeyIndex % apiKeysList.length];
panelState.currentApiKeyIndex = (panelState.currentApiKeyIndex + 1) % apiKeysList.length;
saveAllSettings(); // Save state after incrementing index
updateRealApiKey(); // Update the displayed key in the panel
currentUrlObj.searchParams.set('key', nextApiKey);
} else if (realApiKey) {
// For non-generateContent API calls (e.g., get models list) in cyclic mode, use the current active key
currentUrlObj.searchParams.set('key', realApiKey);
}
} else {
// If not using cyclic API, ensure the single stored API key is used if not already present
if (!currentUrlObj.searchParams.has('key')) {
const singleApiKey = localStorage.getItem(STORAGE_SINGLE_API_KEY) || '';
if (singleApiKey) {
currentUrlObj.searchParams.set('key', singleApiKey);
}
}
}
// Update the request URL with potentially new API key and model version
requestUrl = currentUrlObj.toString();
requestUrl = replaceUrlParts(requestUrl, panelState.currentModel);
// --- Request Body Modification Logic (for generationConfig, safetySettings, thinkingConfig) ---
if (requestInit.body && typeof requestInit.body === 'string' &&
(requestInit.body.includes('"generationConfig"') || requestInit.body.includes('"safetySettings"'))) {
try {
const requestBody = JSON.parse(requestInit.body);
// THIS IS THE BLOCK YOU ASKED ABOUT! It fetches the current settings or uses defaults.
// It is crucial for applying your configured parameters.
const s = allSettings[panelState.currentModel] || {
temperature: 2,
maxOutputTokens: 65536,
topP: 0.95,
topK: 0,
candidateCount: 1,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
safetySettingsThreshold: 'BLOCK_NONE',
thinkingBudget: DEFAULT_THINKING_BUDGET,
includeThoughts: DEFAULT_INCLUDE_THOUGHTS,
overrideThinkingBudget: DEFAULT_OVERRIDE_THINKING_BUDGET
};
// Apply generation configuration parameters
if (!requestBody.generationConfig) requestBody.generationConfig = {};
requestBody.generationConfig.temperature = s.temperature;
requestBody.generationConfig.maxOutputTokens = s.maxOutputTokens;
requestBody.generationConfig.topP = s.topP;
requestBody.generationConfig.topK = s.topK;
requestBody.generationConfig.candidateCount = s.candidateCount;
requestBody.generationConfig.frequencyPenalty = s.frequencyPenalty;
requestBody.generationConfig.presencePenalty = s.presencePenalty;
// Apply safety settings based on the selected threshold
const selectedThreshold = s.safetySettingsThreshold;
if (selectedThreshold) {
requestBody.safetySettings = HARM_CATEGORIES.map(category => ({
category: category,
threshold: selectedThreshold
}));
} else {
// If no threshold selected, remove safetySettings from the request
delete requestBody.safetySettings;
}
// Apply thinkingConfig logic based on the override toggle
if (s.overrideThinkingBudget) {
const thinkingBudget = s.thinkingBudget;
const includeThoughts = s.includeThoughts;
// Add thinkingConfig only if budget is not default or thoughts are included
if (thinkingBudget || includeThoughts) {
requestBody.generationConfig.thinkingConfig = { thinkingBudget: thinkingBudget };
if (includeThoughts) {
requestBody.generationConfig.thinkingConfig.includeThoughts = true;
}
} else {
// If override is on but budget/thoughts are default/off, remove thinkingConfig
delete requestBody.generationConfig.thinkingConfig;
}
} else {
// If override is off, ensure thinkingConfig is not in the request
delete requestBody.generationConfig.thinkingConfig;
}
// Update the request body string and Content-Length header
const newBodyString = JSON.stringify(requestBody);
requestInit.body = newBodyString;
if (requestInit.headers) {
const headers = new Headers(requestInit.headers);
if (headers.has('Content-Length')) {
headers.set('Content-Length', newBodyString.length.toString());
requestInit.headers = headers;
}
}
} catch(e) {
console.error('Error processing request body for generationConfig/safetySettings/thinkingConfig:', e);
// If an error occurs during body modification, proceed with the original body
}
}
// Await the original fetch call with potentially modified URL and requestInit
const response = await originalFetch(requestUrl, requestInit);
// --- Response Interception and Processing (for combining thoughts) ---
// Fetch current settings again to check `includeThoughts` and `overrideThinkingBudget`
const s = allSettings[panelState.currentModel] || {}; // This is the second instance of the settings fetch
// Only process response if it's a 'generateContent' call, successful, and thoughts are enabled via override
if (response.ok && requestUrl.includes('generateContent?') && s.overrideThinkingBudget && s.includeThoughts) {
try {
const data = await response.json(); // Parse the response JSON
// Check if the response contains multiple parts (thought and text)
if (data.candidates && data.candidates[0] && data.candidates[0].content && Array.isArray(data.candidates[0].content.parts) && data.candidates[0].content.parts.length > 1) {
const parts = data.candidates[0].content.parts;
const thoughtPart = parts.find(p => p.thought === true); // Find the thought part
const textPart = parts.find(p => !p.thought); // Find the main text part
if (thoughtPart && textPart) {
// Combine thought and text into a single part
const combinedText = `${thoughtPart.text}\n\n***\n\n${textPart.text}`;
data.candidates[0].content.parts = [{ text: combinedText }];
// Create a new response with the modified body and updated Content-Length header
const newBody = JSON.stringify(data);
const newHeaders = new Headers(response.headers);
newHeaders.set('Content-Length', newBody.length.toString());
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
}
} catch(e) {
console.error("Error processing Gemini response to combine thoughts:", e);
// If any error occurs during response processing, return the original response
// (Note: response.json() consumes the body, so cloning 'response' before consumption is ideal
// if you need to return it as-is upon error, but for this userscript context,
// returning the original fetch's result (which might already be consumed) is acceptable
// as a fallback if processing fails.)
}
}
// Return the original (or minimally modified) response if no specific processing was needed or applicable
return response;
};
toggleBtn.onclick = () => {
panelState.collapsed = !panelState.collapsed;
panel.classList.toggle('collapsed');
saveAllSettings();
};
modelSelect.onchange = () => {
panelState.currentModel = modelSelect.value;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
};
btnToggleThinkingParams.onclick = () => {
panelState.thinkingParamsCollapsed = !panelState.thinkingParamsCollapsed;
updateThinkingParamsVisibility();
saveAllSettings();
};
linkInputs(elems.temperature.num, elems.temperature.range, 0, 2, 0.01);
linkInputs(elems.maxTokens.num, elems.maxTokens.range, 1, 65536, 1);
linkInputs(elems.topP.num, elems.topP.range, 0, 1, 0.01);
linkInputs(elems.topK.num, elems.topK.range, 0, 1000, 1);
linkInputs(elems.candidateCount.num, elems.candidateCount.range, 1, 8, 1);
linkInputs(elems.frequencyPenalty.num, elems.frequencyPenalty.range, -2.0, 2.0, 0.01);
linkInputs(elems.presencePenalty.num, elems.presencePenalty.range, -2.0, 2.0, 0.01);
linkInputs(elems.thinkingBudget.num, elems.thinkingBudget.range, -1, 32768, 1);
btnGetModels.onclick = fetchModelsFromApi;
btnSaveSettings.onclick = () => {
saveModelSettings(modelSelect.value);
};
function fillPresetSelect() {
presetSelect.innerHTML = '';
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Select Preset';
presetSelect.appendChild(opt);
if (allSettings.presets) {
allSettings.presets.forEach(p => {
const opt = document.createElement('option');
opt.value = p.name;
opt.textContent = p.name;
presetSelect.appendChild(opt);
});
}
if (panelState.currentPreset) {
presetSelect.value = panelState.currentPreset;
}
}
function loadPreset(preset) {
const model = preset.model;
allSettings[model] = {
temperature: preset.settings.temperature,
maxOutputTokens: preset.settings.maxOutputTokens,
topP: preset.settings.topP,
topK: preset.settings.topK,
candidateCount: preset.settings.candidateCount !== undefined ? preset.settings.candidateCount : 1,
frequencyPenalty: preset.settings.frequencyPenalty !== undefined ? preset.settings.frequencyPenalty : 0.0,
presencePenalty: preset.settings.presencePenalty !== undefined ? preset.settings.presencePenalty : 0.0,
safetySettingsThreshold: preset.settings.safetySettingsThreshold !== undefined ? preset.settings.safetySettingsThreshold : 'BLOCK_NONE',
thinkingBudget: preset.settings.thinkingBudget !== undefined ? preset.settings.thinkingBudget : DEFAULT_THINKING_BUDGET,
includeThoughts: preset.settings.includeThoughts !== undefined ? preset.settings.includeThoughts : DEFAULT_INCLUDE_THOUGHTS,
overrideThinkingBudget: preset.settings.overrideThinkingBudget !== undefined ? preset.settings.overrideThinkingBudget : DEFAULT_OVERRIDE_THINKING_BUDGET
};
if (model === 'custom') {
allSettings.customModelString = preset.settings.customModelString || '';
}
panelState.currentModel = model;
panelState.currentPreset = preset.name;
modelSelect.value = model;
updateCustomModelInputVisibility();
loadModelSettings(model);
saveAllSettings();
}
presetSelect.onchange = () => {
const name = presetSelect.value;
if (name) {
const preset = allSettings.presets.find(p => p.name === name);
if (preset) loadPreset(preset);
} else {
panelState.currentPreset = null;
loadModelSettings(panelState.currentModel);
saveAllSettings();
}
};
btnAddPreset.onclick = () => {
const name = prompt('Enter preset name:');
if (name) {
if (!allSettings.presets) allSettings.presets = [];
if (allSettings.presets.some(p => p.name === name)) {
alert('A preset with this name already exists. Please choose a different name.');
return;
}
const settings = getCurrentSettings();
const preset = { name, model: panelState.currentModel, settings };
allSettings.presets.push(preset);
saveAllSettings();
fillPresetSelect();
presetSelect.value = name;
panelState.currentPreset = name;
}
};
btnDeletePreset.onclick = () => {
const name = presetSelect.value;
if (name && confirm(`Are you sure you want to delete preset "${name}"?`)) {
allSettings.presets = allSettings.presets.filter(p => p.name !== name);
if (panelState.currentPreset === name) {
panelState.currentPreset = null;
}
saveAllSettings();
fillPresetSelect();
if (!panelState.currentPreset) {
loadModelSettings(panelState.currentModel);
}
}
};
btnResetSettings.onclick = () => {
const defaultSettings = {
temperature: 2.0,
maxOutputTokens: 65536,
topP: 0.95,
topK: 0,
candidateCount: 1,
frequencyPenalty: 0.0,
presencePenalty: 0.0,
safetySettingsThreshold: 'BLOCK_NONE',
thinkingBudget: DEFAULT_THINKING_BUDGET,
includeThoughts: DEFAULT_INCLUDE_THOUGHTS,
overrideThinkingBudget: DEFAULT_OVERRIDE_THINKING_BUDGET
};
elems.temperature.num.value = defaultSettings.temperature;
elems.temperature.range.value = defaultSettings.temperature;
elems.maxTokens.num.value = defaultSettings.maxOutputTokens;
elems.maxTokens.range.value = defaultSettings.maxOutputTokens;
elems.topP.num.value = defaultSettings.topP;
elems.topP.range.value = defaultSettings.topP;
elems.topK.num.value = defaultSettings.topK;
elems.topK.range.value = defaultSettings.topK;
elems.candidateCount.num.value = defaultSettings.candidateCount;
elems.candidateCount.range.value = defaultSettings.candidateCount;
elems.frequencyPenalty.num.value = defaultSettings.frequencyPenalty;
elems.frequencyPenalty.range.value = defaultSettings.frequencyPenalty;
elems.presencePenalty.num.value = defaultSettings.presencePenalty;
elems.presencePenalty.range.value = defaultSettings.presencePenalty;
safetySettingsSelect.value = defaultSettings.safetySettingsThreshold;
elems.thinkingBudget.num.value = defaultSettings.thinkingBudget;
elems.thinkingBudget.range.value = defaultSettings.thinkingBudget;
toggleIncludeThoughts.checked = defaultSettings.includeThoughts;
toggleOverrideThinkingBudget.checked = defaultSettings.overrideThinkingBudget;
if (panelState.currentModel === 'custom') customModelInput.value = '';
allSettings[panelState.currentModel] = { ...defaultSettings };
if (panelState.currentModel === 'custom') allSettings.customModelString = '';
panelState.currentPreset = null;
fillPresetSelect();
updateThinkingControlsState();
saveAllSettings();
};
btnExportSettings.onclick = () => {
const exportData = {
settings: allSettings,
panelState: panelState,
singleApiKey: localStorage.getItem(STORAGE_SINGLE_API_KEY),
apiKeysList: localStorage.getItem(STORAGE_API_KEY_LIST)
};
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chub_gemini_settings.json';
a.click();
URL.revokeObjectURL(url);
};
btnImportSettings.onclick = () => {
inputImportSettings.click();
};
inputImportSettings.onchange = () => {
const file = inputImportSettings.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
if (importedData.settings) allSettings = importedData.settings;
if (importedData.panelState) {
panelState = {
collapsed: true, currentModel: DEFAULT_MODEL, currentPreset: null,
apiVersion: DEFAULT_API_VERSION, useCyclicApi: DEFAULT_USE_CYCLIC_API,
currentApiKeyIndex: DEFAULT_CURRENT_API_KEY_INDEX, thinkingParamsCollapsed: true,
...importedData.panelState
};
}
if (importedData.singleApiKey !== undefined) localStorage.setItem(STORAGE_SINGLE_API_KEY, importedData.singleApiKey);
if (importedData.apiKeysList !== undefined) localStorage.setItem(STORAGE_API_KEY_LIST, importedData.apiKeysList);
loadGlobalApiKeySettings();
saveAllSettings();
fillModelSelect();
fillPresetSelect();
modelList = (allSettings.modelList && Array.isArray(allSettings.modelList)) ? allSettings.modelList : [];
if (panelState.currentPreset) {
const preset = allSettings.presets && allSettings.presets.find(p => p.name === panelState.currentPreset);
if (preset) {
loadPreset(preset);
} else {
panelState.currentPreset = null;
modelSelect.value = DEFAULT_MODEL;
panelState.currentModel = DEFAULT_MODEL;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
}
} else if(panelState.currentModel && modelList.includes(panelState.currentModel)) {
modelSelect.value = panelState.currentModel;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
} else {
modelSelect.value = DEFAULT_MODEL;
panelState.currentModel = DEFAULT_MODEL;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
}
updateThinkingParamsVisibility();
alert('Settings successfully imported.');
} catch (err) {
alert('Error importing settings: ' + err.message);
console.error('Import error:', err);
}
};
reader.readAsText(file);
}
inputImportSettings.value = '';
};
// --- Initial Load Sequence ---
loadPanelState();
loadGlobalApiKeySettings();
loadAllSettings();
apiVersionSelect.value = panelState.apiVersion || DEFAULT_API_VERSION;
modelList = allSettings.modelList || [];
fillModelSelect();
fillPresetSelect();
if(panelState.currentPreset) {
const preset = allSettings.presets && allSettings.presets.find(p => p.name === panelState.currentPreset);
if (preset) {
loadPreset(preset);
} else {
panelState.currentPreset = null;
modelSelect.value = DEFAULT_MODEL;
panelState.currentModel = DEFAULT_MODEL;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
}
} else if(panelState.currentModel && modelList.includes(panelState.currentModel)) {
modelSelect.value = panelState.currentModel;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
} else {
modelSelect.value = DEFAULT_MODEL;
panelState.currentModel = DEFAULT_MODEL;
updateCustomModelInputVisibility();
loadModelSettings(panelState.currentModel);
}
panel.classList.toggle('collapsed', panelState.collapsed);
updateThinkingParamsVisibility();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
createPanel();
} else {
document.addEventListener('DOMContentLoaded', createPanel);
}
})();