Chub AI palm2 model list Enhancer

Gemini Settings Panel: API model selection, parameters, presets, reset settings, tooltips, export/import settings, enhanced parameters and safety settings

目前為 2025-07-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Chub AI palm2 model list Enhancer
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      4.04
// @description  Gemini Settings Panel: API model selection, parameters, presets, reset settings, tooltips, export/import settings, enhanced parameters and safety settings
// @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_API_KEY = 'chubGeminiApiKey';

    // --- Defaults ---
    const DEFAULT_MODEL = 'custom';
    const API_MODELS_URL_BASE = 'https://generativelanguage.googleapis.com/v1beta/models?key=';

    // --- 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};
    let modelList = [];
    let apiKey = '';

    // --- 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>
        <h4>Gemini Settings</h4>
        <label>API Key:
            <input type="password" id="api-key-input" autocomplete="off" placeholder="Insert API key here" />
        </label>
        <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>
        <label>Model:
            <select id="model-select"></select>
            <input type="text" id="custom-model-input" placeholder="Enter your model" style="display:none;" />
        </label>

        <!-- 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>
        `;

        document.body.appendChild(panel);

        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: calc(min(1.5vw, 8px) * var(--scale-factor)) calc(min(2vw, 10px) * var(--scale-factor)); /* Reduced padding */
            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;
        }

        #gemini-settings-panel:not(.collapsed) {
            transform: translateY(-50%) translateX(0);
        }

        #gemini-settings-panel h4 {
            text-align: center;
            margin: 0 0 calc(min(1.5vw, 6px) * var(--scale-factor)); /* Reduced margin */
            font-size: calc(min(3vw, 16px) * var(--scale-factor));
        }

        #gemini-settings-panel label {
            display: block;
            margin-bottom: calc(min(1vw, 5px) * var(--scale-factor)); /* Reduced margin */
            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)); /* Reduced padding */
            font-size: calc(min(2.3vw, 13px) * var(--scale-factor));
            width: 100%;
            box-sizing: border-box;
            margin-top: calc(2px * var(--scale-factor)); /* Reduced margin */
        }

        .param-group {
            margin-bottom: calc(min(1.5vw, 8px) * var(--scale-factor)); /* Reduced margin */
        }

        .param-group label {
            display: block;
            margin-bottom: calc(min(0.5vw, 2px) * var(--scale-factor)); /* Reduced margin */
            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, 4px) * var(--scale-factor)); /* Reduced gap */
            margin-top: calc(0.2vw * var(--scale-factor)); /* Reduced margin */
        }

        .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)); /* Reduced padding */
            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)); /* Reduced margin */
            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 {
            width: 100%;
            padding: calc(min(1vw, 5px) * var(--scale-factor)); /* Reduced padding */
            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.8vw, 4px) * var(--scale-factor)); /* Reduced margin */
            transition: background-color 0.3s ease;
            font-size: calc(min(2.5vw, 14px) * var(--scale-factor));
        }

        /* Specific margin for Get models list button, also reduced */
        #btn-get-models {
            margin-bottom: calc(min(1.5vw, 8px) * var(--scale-factor));
        }

        /* Specific margin for custom model input */
        #custom-model-input {
            margin-top: calc(2px * var(--scale-factor)); /* Consistent with other inputs */
        }


        #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 {
            background: #388e3c;
        }

        #save-toast {
            margin-top: calc(min(1.5vw, 6px) * var(--scale-factor)); /* Reduced margin */
            text-align: center;
            background: #222;
            color: #0f0;
            padding: calc(min(0.8vw, 4px) * var(--scale-factor)); /* Reduced padding */
            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);
        }

        @media screen and (max-width: 600px) {
            #gemini-settings-panel {
                max-width: calc(80vw * var(--scale-factor));
                padding: calc(min(2vw, 8px) * var(--scale-factor)) calc(min(3vw, 10px) * var(--scale-factor)); /* Adjusted for small screens */
                font-size: calc(min(3vw, 12px) * var(--scale-factor));
            }

            #gemini-settings-panel h4 {
                font-size: calc(min(3.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 {
                font-size: calc(min(2.8vw, 11px) * var(--scale-factor));
                padding: calc(min(0.6vw, 2px) * var(--scale-factor)) calc(min(1vw, 4px) * var(--scale-factor));
            }

            #btn-save-settings, #btn-get-models, #btn-add-preset, #btn-delete-preset, #btn-reset-settings, #btn-export-settings, #btn-import-settings {
                padding: calc(min(1.5vw, 5px) * var(--scale-factor));
                font-size: calc(min(3vw, 12px) * var(--scale-factor));
            }
        }
        `;
        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 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 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');

        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') }
        };

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

        document.addEventListener('click', (event) => {
            if (!panel.contains(event.target) && !toggleBtn.contains(event.target) && !panelState.collapsed) {
                panelState.collapsed = true;
                panel.classList.add('collapsed');
                saveAllSettings();
            }
        });

        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 loadApiKey() {
            const storedKey = localStorage.getItem(STORAGE_API_KEY) || '';
            realApiKey = storedKey;
            apiKey = storedKey;
            apiKeyInput.value = maskKeyDisplay(realApiKey);
        }
        function saveApiKey(newKey) {
            apiKey = newKey.trim();
            localStorage.setItem(STORAGE_API_KEY, apiKey);
        }

        let realApiKey = apiKey;
        apiKeyInput.addEventListener('focus', () => {
            apiKeyInput.type = 'text';
            apiKeyInput.value = realApiKey;
        });
        apiKeyInput.addEventListener('blur', () => {
            saveApiKey(apiKeyInput.value);
            realApiKey = apiKeyInput.value.trim();
            apiKeyInput.type = 'password';
            apiKeyInput.value = maskKeyDisplay(realApiKey);
        });
        loadApiKey();

        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 loadModelSettings(model) {
            if (!model) model = DEFAULT_MODEL;
            const settings = allSettings[model] || {
                temperature: 2.0,
                maxOutputTokens: 65536,
                topP: 0.95,
                topK: 0,
                candidateCount: 1, // Default for new param
                frequencyPenalty: 0.0, // Default for new param
                presencePenalty: 0.0, // Default for new param
                safetySettingsThreshold: 'BLOCK_NONE' // Default for new param
            };
            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;

            if (model === 'custom') {
                customModelInput.value = allSettings.customModelString || '';
            } else {
                customModelInput.value = '';
            }
        }

        function getCurrentSettings() {
            return {
                temperature: clamp(parseFloat(elems.temperature.num.value), 0, 2),
                maxOutputTokens: clamp(parseInt(elems.maxTokens.num.value), 0, 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), // Candidate count is usually 1 for Gemini
                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()
            };
        }

        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
            };
            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 state = JSON.parse(s);
                    panelState = {...panelState, ...state};
                }
            } 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() {
            if(!realApiKey) {
                alert('Please enter an API key');
                return;
            }
            btnGetModels.disabled = true;
            btnGetModels.textContent = 'Loading...';
            try {
                const response = await fetch(API_MODELS_URL_BASE + encodeURIComponent(realApiKey));
                if(!response.ok) throw new Error('Network response was not ok');
                const data = await response.json();
                if(data.models && Array.isArray(data.models)) {
                    modelList = data.models
                        .map(m => m.name)
                        // Удалена строка .filter(name => name.startsWith('models/gemini-'))
                        // Улучшена логика удаления префикса models/
                        .map(name => {
                            const prefix = 'models/';
                            return name.startsWith(prefix) ? name.substring(prefix.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 replaceModelInUrl(url, modelName) {
            if(typeof url !== 'string') return url;
            if(modelName === 'custom') {
                modelName = allSettings.customModelString || '';
                if(!modelName) return url;
            }
            // Ensure we only replace if the model part is actually present
            // This regex specifically targets 'models/something:' or 'models/something'
            return url.replace(/(models\/[^:]+)(:)?/, (m, p1, p2) => {
                // p1 is 'models/old-model-name', p2 is ':' or undefined
                const newModelPath = 'models/' + modelName;
                return newModelPath + (p2 || '');
            });
        }


        const originalFetch = window.fetch;
        window.fetch = async function(input, init) {
            let url = input;
            if(typeof url === 'string' && url.includes('generativelanguage.googleapis.com')) {
                url = replaceModelInUrl(url, panelState.currentModel);
            }
            if(init && init.body && typeof init.body === 'string' && (init.body.includes('"generationConfig"') || init.body.includes('"safetySettings"'))) {
                try {
                    const requestBody = JSON.parse(init.body);
                    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'
                    };

                    // Apply generation config 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
                    const selectedThreshold = s.safetySettingsThreshold;
                    if (selectedThreshold && selectedThreshold !== 'BLOCK_NONE') { // Only apply if not BLOCK_NONE
                        requestBody.safetySettings = HARM_CATEGORIES.map(category => ({
                            category: category,
                            threshold: selectedThreshold
                        }));
                    } else {
                        // If 'BLOCK_NONE' or no specific threshold, ensure safetySettings are not sent or default
                        delete requestBody.safetySettings;
                    }

                    init.body = JSON.stringify(requestBody);
                } catch(e) {
                    console.error('Error processing request body:', e);
                }
            }
            return originalFetch(url, init);
        };

        toggleBtn.onclick = () => {
            panelState.collapsed = !panelState.collapsed;
            if(panelState.collapsed) panel.classList.add('collapsed');
            else panel.classList.remove('collapsed');
            saveAllSettings();
        };

        modelSelect.onchange = () => {
            panelState.currentModel = modelSelect.value;
            updateCustomModelInputVisibility();
            loadModelSettings(panelState.currentModel);
        };

        linkInputs(elems.temperature.num, elems.temperature.range, 0, 2, 0.01);
        linkInputs(elems.maxTokens.num, elems.maxTokens.range, 0, 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); // Candidate count is 1 for Gemini, can't be changed by user
        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);

        btnGetModels.onclick = fetchModelsFromApi;

        btnSaveSettings.onclick = () => {
            saveModelSettings(modelSelect.value);
        };

        // Preset management
        function fillPresetSelect() {
            presetSelect.innerHTML = '';
            const opt = document.createElement('option');
            opt.value = '';
            opt.textContent = 'Select preset';
            presetSelect.appendChild(opt);
            if (allSettings.presets) {
                for (const p of allSettings.presets) {
                    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;
            // Apply settings from preset to current model's settings
            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, // Handle potential undefined from older presets
                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'
            };
            if (model === 'custom') {
                allSettings.customModelString = preset.settings.customModelString || '';
            }
            panelState.currentModel = model;
            panelState.currentPreset = preset.name;
            modelSelect.value = model;
            updateCustomModelInputVisibility();
            loadModelSettings(model);
            saveAllSettings(); // Save after loading preset
        }

        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;
                // When "Select preset" is chosen, reload settings for the current model without preset
                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('Preset with this name already exists. Please choose a different name.');
                    return;
                }
                const settings = getCurrentSettings();
                const preset = {
                    name,
                    model: panelState.currentModel,
                    settings // Store current settings with the preset
                };
                allSettings.presets.push(preset);
                saveAllSettings();
                fillPresetSelect();
                presetSelect.value = name; // Select the newly added preset
                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();
                // After deleting, reload current model settings if no preset is selected
                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'
            };
            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;

            if (panelState.currentModel === 'custom') {
                customModelInput.value = '';
            }
            // Update the allSettings for the current model with defaults
            allSettings[panelState.currentModel] = { ...defaultSettings };
            if (panelState.currentModel === 'custom') {
                allSettings.customModelString = '';
            }
            panelState.currentPreset = null; // Unselect any active preset
            fillPresetSelect(); // Refresh preset dropdown
            saveAllSettings();
        };

        btnExportSettings.onclick = () => {
            const json = JSON.stringify(allSettings, 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 = '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 json = JSON.parse(e.target.result);
                        // Merge imported settings with existing ones, or overwrite completely
                        allSettings = json;
                        saveAllSettings();
                        fillModelSelect();
                        fillPresetSelect();
                        // Re-evaluate current model and state after import
                        if (allSettings.modelList && Array.isArray(allSettings.modelList)) {
                            modelList = allSettings.modelList;
                        } else {
                            modelList = [];
                        }
                        if (panelState.currentModel && modelList.includes(panelState.currentModel)) {
                            modelSelect.value = panelState.currentModel;
                        } else {
                            modelSelect.value = DEFAULT_MODEL;
                            panelState.currentModel = DEFAULT_MODEL;
                        }
                        updateCustomModelInputVisibility();
                        loadModelSettings(panelState.currentModel);
                        alert('Settings successfully imported');
                    } catch (err) {
                        alert('Error importing settings: ' + err.message);
                    }
                };
                reader.readAsText(file);
            }
            inputImportSettings.value = ''; // Clear the file input
        };

        // Initial load
        loadPanelState();
        loadAllSettings();
        loadApiKey();
        if(allSettings.modelList) {
            modelList = allSettings.modelList;
        }
        fillModelSelect();
        fillPresetSelect(); // Fill presets after allSettings are loaded
        if(panelState.currentPreset) { // If a preset was active, try to load it
            const preset = allSettings.presets && allSettings.presets.find(p => p.name === panelState.currentPreset);
            if (preset) {
                loadPreset(preset); // This will set modelSelect.value and load settings
            } else { // Preset not found, revert to default model
                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);
        }

        if(panelState.collapsed) {
            panel.classList.add('collapsed');
        } else {
            panel.classList.remove('collapsed');
        }

        // Адаптивный размер панели в зависимости от уровня зума (оставлено без изменений)
        function updatePanelSize() {
            const scale = window.visualViewport ? window.visualViewport.scale : 1;
            const originalWidth = 300; // исходная ширина панели в пикселях
            const originalToggleWidth = 28; // исходная ширина кнопки в пикселях
            const originalToggleHeight = 48; // исходная высота кнопки в пикселях

            // Применяем scale-factor, а не напрямую уменьшаем ширину, чтобы сохранить пропорции
            // CSS уже использует var(--scale-factor), так что здесь может быть избыточно
            // Этот блок кода можно удалить, если CSS справляется сам
        }

        // Инициализация размера панели
        updatePanelSize();

        // Добавление слушателя событий для изменения зума
        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', updatePanelSize);
        } else {
            // Резервный вариант для браумеров без visualViewport
            window.addEventListener('resize', updatePanelSize);
        }
    }

    // Ensure the panel is created after the DOM is fully loaded
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        createPanel();
    } else {
        document.addEventListener('DOMContentLoaded', createPanel);
    }
})();