Chub AI Gemini/PaLM2 Model List Enhancer (Refactored)

Gemini Settings Panel: API version/model selection, parameters, presets, reset, tooltips, export/import, enhanced safety settings, and thinking mode options.

当前为 2025-07-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Chub AI Gemini/PaLM2 Model List Enhancer (Refactored)
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      6.4
// @description  Gemini Settings Panel: API version/model selection, parameters, presets, reset, tooltips, export/import, enhanced safety settings, and thinking mode options.
// @author       Ko16aska (Refactored by Assistant)
// @match        *://chub.ai/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration & Constants ---
    const STORAGE_KEYS = {
        SETTINGS: 'chubGeminiSettings',
        PANEL_STATE: 'chubGeminiPanelState',
        SINGLE_API_KEY: 'chubGeminiApiKey',
        API_KEY_LIST: 'chubGeminiApiKeysList'
    };

    const DEFAULTS = {
        MODEL: 'custom',
        API_VERSION: 'v1beta',
        USE_CYCLIC_API: false,
        CURRENT_API_KEY_INDEX: 0,
        THINKING_BUDGET: -1, // -1 for Auto
        INCLUDE_THOUGHTS: false,
        OVERRIDE_THINKING_BUDGET: false
    };

    const MODEL_SETTINGS_DEFAULTS = {
        temperature: 2.0,
        maxOutputTokens: 65536,
        topP: 0.95,
        topK: 0,
        candidateCount: 1,
        frequencyPenalty: 0.0,
        presencePenalty: 0.0,
        safetySettingsThreshold: 'BLOCK_NONE',
        thinkingBudget: DEFAULTS.THINKING_BUDGET,
        includeThoughts: DEFAULTS.INCLUDE_THOUGHTS,
        overrideThinkingBudget: DEFAULTS.OVERRIDE_THINKING_BUDGET
    };

    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 = {};
    let modelList = [];
    let apiKeysList = [];
    let realApiKey = ''; // The actual API key used for requests
    let toastTimeout = null;

    // --- Core Functions ---

    /**
     * Creates and injects the main panel and its styles into the DOM.
     */
    function initializePanel() {
        const panel = document.createElement('div');
        panel.id = 'gemini-settings-panel';
        panel.innerHTML = buildPanelHTML();
        document.body.appendChild(panel);

        const apiKeyListModal = document.createElement('div');
        apiKeyListModal.id = 'api-key-list-modal';
        apiKeyListModal.style.display = 'none';
        apiKeyListModal.innerHTML = buildApiKeyModalHTML();
        document.body.appendChild(apiKeyListModal);

        const style = document.createElement('style');
        style.textContent = getPanelStyle();
        document.head.appendChild(style);

        const domElements = queryDOMElements(panel, apiKeyListModal);

        loadState();
        setupInitialUI(domElements);
        registerEventListeners(domElements);
        applyZoomListener();
    }

    /**
     * Loads the state from localStorage.
     */
    function loadState() {
        try {
            const storedSettings = localStorage.getItem(STORAGE_KEYS.SETTINGS);
            allSettings = storedSettings ? JSON.parse(storedSettings) : { presets: [], modelList: [] };
            modelList = allSettings.modelList || [];

            const storedPanelState = localStorage.getItem(STORAGE_KEYS.PANEL_STATE);
            panelState = {
                collapsed: true,
                currentModel: DEFAULTS.MODEL,
                currentPreset: null,
                apiVersion: DEFAULTS.API_VERSION,
                useCyclicApi: DEFAULTS.USE_CYCLIC_API,
                currentApiKeyIndex: DEFAULTS.CURRENT_API_KEY_INDEX,
                thinkingParamsCollapsed: true,
                ...(storedPanelState ? JSON.parse(storedPanelState) : {})
            };

            const storedKeysList = localStorage.getItem(STORAGE_KEYS.API_KEY_LIST) || '';
            apiKeysList = storedKeysList.split('\n').map(k => k.trim()).filter(k => k);
        } catch (e) {
            console.error('Error loading state from localStorage:', e);
        }
    }

    /**
     * Saves the current state to localStorage.
     */
    function saveState() {
        try {
            localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(allSettings));
            localStorage.setItem(STORAGE_KEYS.PANEL_STATE, JSON.stringify(panelState));
        } catch (e) {
            console.error('Error saving state to localStorage:', e);
        }
    }

    /**
     * Sets up the initial UI state based on loaded data.
     * @param {object} dom - The object containing all DOM elements.
     */
    function setupInitialUI(dom) {
        dom.panel.classList.toggle('collapsed', panelState.collapsed);
        dom.apiVersionSelect.value = panelState.apiVersion;
        dom.toggleCyclicApi.checked = panelState.useCyclicApi;

        fillModelSelect(dom.modelSelect);
        fillPresetSelect(dom.presetSelect);
        updateApiKeyUI(dom.apiKeyInput);
        updateThinkingParamsVisibility(dom.thinkingModeParamsDiv, dom.btnToggleThinkingParams);

        applyCurrentSettingsToUI(dom);
    }

    /**
     * Applies the settings for the current model or preset to the UI.
     * @param {object} dom - The object containing all DOM elements.
     */
    function applyCurrentSettingsToUI(dom) {
        const preset = panelState.currentPreset ? allSettings.presets.find(p => p.name === panelState.currentPreset) : null;

        if (preset) {
            loadPreset(preset, dom);
        } else {
            const modelExists = modelList.includes(panelState.currentModel);
            const currentModel = modelExists ? panelState.currentModel : DEFAULTS.MODEL;
            panelState.currentModel = currentModel;
            dom.modelSelect.value = currentModel;
            loadModelSettings(currentModel, dom);
        }
        updateCustomModelInputVisibility(dom.modelSelect, dom.customModelInput);
    }

    // --- UI Update Functions ---

    function fillModelSelect(select) {
        select.innerHTML = '<option value="custom">Custom</option>';
        modelList.forEach(m => {
            const opt = new Option(m, m);
            select.appendChild(opt);
        });
    }

    function fillPresetSelect(select) {
        select.innerHTML = '<option value="">Select Preset</option>';
        (allSettings.presets || []).forEach(p => {
            const opt = new Option(p.name, p.name);
            select.appendChild(opt);
        });
        select.value = panelState.currentPreset || '';
    }

    function updateApiKeyUI(apiKeyInput) {
        if (panelState.useCyclicApi && apiKeysList.length > 0) {
            realApiKey = apiKeysList[panelState.currentApiKeyIndex % apiKeysList.length];
            apiKeyInput.disabled = true;
            apiKeyInput.type = 'text';
            apiKeyInput.value = realApiKey;
            apiKeyInput.title = 'Active key from list (disabled in cyclic mode). Use "Manage Keys" to edit.';
        } else {
            realApiKey = localStorage.getItem(STORAGE_KEYS.SINGLE_API_KEY) || '';
            apiKeyInput.disabled = false;
            apiKeyInput.type = 'password';
            apiKeyInput.value = maskKeyDisplay(realApiKey);
            apiKeyInput.title = '';
        }
    }

    function updateThinkingParamsVisibility(container, button) {
        container.style.display = panelState.thinkingParamsCollapsed ? 'none' : 'block';
        button.textContent = panelState.thinkingParamsCollapsed ? '🧠' : '🧠'; // Could add rotation/style change
    }

    function updateCustomModelInputVisibility(modelSelect, customInput) {
        customInput.style.display = modelSelect.value === 'custom' ? 'block' : 'none';
    }

    function updateThinkingControlsState(dom) {
        const isEnabled = dom.toggleOverrideThinkingBudget.checked;
        const elementsToToggle = [
            dom.thinkingBudgetGroup,
            dom.includeThoughtsLabel,
            dom.elems.thinkingBudget.num,
            dom.elems.thinkingBudget.range,
            dom.toggleIncludeThoughts
        ];
        elementsToToggle.forEach(el => {
            el.classList.toggle('disabled', !isEnabled);
            if (el.tagName === 'INPUT') el.disabled = !isEnabled;
        });
    }


    function showSaveToast(toastElement) {
        toastElement.classList.add('show');
        if (toastTimeout) clearTimeout(toastTimeout);
        toastTimeout = setTimeout(() => toastElement.classList.remove('show'), 1800);
    }

    // --- Settings & Preset Logic ---

    function getCurrentModelSettings() {
        return {
            ...MODEL_SETTINGS_DEFAULTS,
            ...(allSettings[panelState.currentModel] || {}),
        };
    }

    function loadModelSettings(model, dom) {
        const settings = { ...MODEL_SETTINGS_DEFAULTS, ...(allSettings[model] || {}) };
        applySettingsToForm(settings, dom);
        if (model === 'custom') {
            dom.customModelInput.value = allSettings.customModelString || '';
        }
        updateThinkingControlsState(dom);
    }

    function saveCurrentModelSettings(dom) {
        const model = dom.modelSelect.value;
        const currentSettings = getSettingsFromForm(dom);

        allSettings[model] = currentSettings;
        if (model === 'custom') {
            allSettings.customModelString = dom.customModelInput.value.trim();
        }

        if (panelState.currentPreset) {
            const preset = allSettings.presets.find(p => p.name === panelState.currentPreset);
            if (preset) {
                preset.model = model;
                preset.settings = currentSettings;
            }
        }

        saveState();
        showSaveToast(dom.saveToast);
    }

    function getSettingsFromForm(dom) {
        return {
            temperature: clamp(parseFloat(dom.elems.temperature.num.value), 0, 2),
            maxOutputTokens: clamp(parseInt(dom.elems.maxTokens.num.value, 10), 1, 65536),
            topP: clamp(parseFloat(dom.elems.topP.num.value), 0, 1),
            topK: clamp(parseInt(dom.elems.topK.num.value, 10), 0, 1000),
            candidateCount: clamp(parseInt(dom.elems.candidateCount.num.value, 10), 1, 8),
            frequencyPenalty: clamp(parseFloat(dom.elems.frequencyPenalty.num.value), -2.0, 2.0),
            presencePenalty: clamp(parseFloat(dom.elems.presencePenalty.num.value), -2.0, 2.0),
            safetySettingsThreshold: dom.safetySettingsSelect.value,
            thinkingBudget: clamp(parseInt(dom.elems.thinkingBudget.num.value, 10), -1, 32768),
            includeThoughts: dom.toggleIncludeThoughts.checked,
            overrideThinkingBudget: dom.toggleOverrideThinkingBudget.checked,
        };
    }

    function applySettingsToForm(settings, dom) {
        dom.elems.temperature.num.value = dom.elems.temperature.range.value = settings.temperature;
        dom.elems.maxTokens.num.value = dom.elems.maxTokens.range.value = settings.maxOutputTokens;
        dom.elems.topP.num.value = dom.elems.topP.range.value = settings.topP;
        dom.elems.topK.num.value = dom.elems.topK.range.value = settings.topK;
        dom.elems.candidateCount.num.value = dom.elems.candidateCount.range.value = settings.candidateCount;
        dom.elems.frequencyPenalty.num.value = dom.elems.frequencyPenalty.range.value = settings.frequencyPenalty;
        dom.elems.presencePenalty.num.value = dom.elems.presencePenalty.range.value = settings.presencePenalty;
        dom.elems.thinkingBudget.num.value = dom.elems.thinkingBudget.range.value = settings.thinkingBudget;
        dom.safetySettingsSelect.value = settings.safetySettingsThreshold;
        dom.toggleIncludeThoughts.checked = settings.includeThoughts;
        dom.toggleOverrideThinkingBudget.checked = settings.overrideThinkingBudget;
    }

    function loadPreset(preset, dom) {
        const model = preset.model || DEFAULTS.MODEL;
        const settings = { ...MODEL_SETTINGS_DEFAULTS, ...preset.settings };

        allSettings[model] = settings;
        if (model === 'custom') {
            allSettings.customModelString = settings.customModelString || '';
        }
        panelState.currentModel = model;
        panelState.currentPreset = preset.name;
        dom.modelSelect.value = model;
        dom.presetSelect.value = preset.name;
        updateCustomModelInputVisibility(dom.modelSelect, dom.customModelInput);
        loadModelSettings(model, dom);
        saveState();
    }

    // --- API & Fetch Override ---

    async function fetchModelsFromApi(dom) {
        if (!realApiKey) {
            alert('Please enter an API key or add keys to the list.');
            return;
        }
        dom.btnGetModels.disabled = true;
        dom.btnGetModels.textContent = 'Loading...';

        try {
            const url = `https://generativelanguage.googleapis.com/${panelState.apiVersion}/models?key=${encodeURIComponent(realApiKey)}`;
            const response = await fetch(url, { bypass: true }); // bypass our own fetch override
            if (!response.ok) throw new Error(`Network error: ${response.statusText}`);
            const data = await response.json();

            modelList = data.models
                .map(m => m.name.replace('models/', ''))
                .filter(m => m);

            allSettings.modelList = modelList;
            fillModelSelect(dom.modelSelect);
            saveState();
        } catch (e) {
            alert('Error loading models: ' + e.message);
            console.error(e);
        } finally {
            dom.btnGetModels.disabled = false;
            dom.btnGetModels.textContent = 'Get Models List';
        }
    }

    const originalFetch = window.fetch;
    window.fetch = async function(input, init) {
        if (init?.bypass) {
            return originalFetch(input, init);
        }

        let requestUrl = (input instanceof Request) ? input.url : input;

        if (!requestUrl.includes('generativelanguage.googleapis.com')) {
            return originalFetch(input, init);
        }

        const requestInit = { ...(input instanceof Request ? await input.clone() : {}), ...init };

        // API Key and URL management
        const url = new URL(requestUrl);
        if (url.pathname.includes('generateContent') && panelState.useCyclicApi && apiKeysList.length > 0) {
            panelState.currentApiKeyIndex = (panelState.currentApiKeyIndex + 1) % apiKeysList.length;
            const nextApiKey = apiKeysList[panelState.currentApiKeyIndex];
            url.searchParams.set('key', nextApiKey);
            saveState();
            // We can't easily update the UI here without a DOM reference. This is a minor decoupling trade-off.
        } else if (!url.searchParams.has('key') && realApiKey) {
            url.searchParams.set('key', realApiKey);
        }

        // Model and version replacement
        let finalUrl = url.toString().replace(/(v1beta|v1)\//, `${panelState.apiVersion}/`);
        const modelToUse = (panelState.currentModel === 'custom') ? (allSettings.customModelString || '') : panelState.currentModel;
        if(modelToUse) {
            finalUrl = finalUrl.replace(/models\/[^:]+/, `models/${modelToUse}`);
        }

        // Body modification
        if (requestInit.body && typeof requestInit.body === 'string') {
            try {
                const body = JSON.parse(requestInit.body);
                const settings = getCurrentModelSettings();

                body.generationConfig = {
                    ...body.generationConfig,
                    temperature: settings.temperature,
                    maxOutputTokens: settings.maxOutputTokens,
                    topP: settings.topP,
                    topK: settings.topK,
                    candidateCount: settings.candidateCount,
                    frequencyPenalty: settings.frequencyPenalty,
                    presencePenalty: settings.presencePenalty,
                };

                body.safetySettings = HARM_CATEGORIES.map(category => ({
                    category,
                    threshold: settings.safetySettingsThreshold,
                }));

                if (settings.overrideThinkingBudget) {
                    body.generationConfig.thinkingConfig = { thinkingBudget: settings.thinkingBudget };
                    if (settings.includeThoughts) {
                        body.generationConfig.thinkingConfig.includeThoughts = true;
                    }
                } else {
                    delete body.generationConfig.thinkingConfig;
                }

                requestInit.body = JSON.stringify(body);
                if (requestInit.headers) {
                    const headers = new Headers(requestInit.headers);
                    headers.set('Content-Length', new Blob([requestInit.body]).size);
                    requestInit.headers = headers;
                }
            } catch (e) {
                console.error("Error modifying request body:", e);
            }
        }

        const response = await originalFetch(finalUrl, requestInit);

        // Response modification for 'thoughts'
        const settings = getCurrentModelSettings();
        if (response.ok && finalUrl.includes('generateContent') && settings.overrideThinkingBudget && settings.includeThoughts) {
            try {
                const data = await response.json();
                const parts = data.candidates?.[0]?.content?.parts;
                if (Array.isArray(parts) && parts.length > 1) {
                    const thought = parts.find(p => p.thought)?.text || '';
                    const text = parts.find(p => !p.thought)?.text || '';
                    if (thought && text) {
                        data.candidates[0].content.parts = [{ text: `${thought}\n\n***\n\n${text}` }];
                        const newBody = JSON.stringify(data);
                        const newHeaders = new Headers(response.headers);
                        newHeaders.set('Content-Length', new Blob([newBody]).size);
                        return new Response(newBody, { status: response.status, statusText: response.statusText, headers: newHeaders });
                    }
                }
            } catch(e) {
                 console.error("Error processing Gemini response to combine thoughts:", e);
            }
        }

        return response;
    };


    // --- Helper Functions ---
    function clamp(val, min, max) {
        return Math.min(max, Math.max(min, val));
    }

    function linkInputs(numInput, rangeInput) {
        const syncValues = (e) => {
            let val = clamp(parseFloat(e.target.value), numInput.min, numInput.max);
            numInput.value = val;
            rangeInput.value = val;
        };
        numInput.addEventListener('input', syncValues);
        rangeInput.addEventListener('input', syncValues);
    }

    function maskKeyDisplay(key) {
        if (!key || key.length <= 4) return '****';
        return key.slice(0, 2) + '*'.repeat(key.length - 4) + key.slice(-2);
    }

    function applyZoomListener() {
        let lastDevicePixelRatio = window.devicePixelRatio;
        const updateScaleFactor = () => {
            document.documentElement.style.setProperty('--scale-factor', 1 / window.devicePixelRatio);
        };
        const checkZoom = () => {
            if (window.devicePixelRatio !== lastDevicePixelRatio) {
                lastDevicePixelRatio = window.devicePixelRatio;
                updateScaleFactor();
            }
            requestAnimationFrame(checkZoom);
        };
        updateScaleFactor();
        checkZoom();
    }

    // --- Event Listeners Registration ---
    function registerEventListeners(dom) {
        dom.toggleBtn.addEventListener('click', () => {
            panelState.collapsed = !panelState.collapsed;
            dom.panel.classList.toggle('collapsed');
            saveState();
        });

        document.addEventListener('click', (event) => {
            const isClickOutside = !dom.panel.contains(event.target) &&
                !dom.toggleBtn.contains(event.target) &&
                !dom.apiKeyListModal.contains(event.target);

            if (isClickOutside && !panelState.collapsed) {
                panelState.collapsed = true;
                dom.panel.classList.add('collapsed');
                saveState();
            }
        });

        dom.apiKeyInput.addEventListener('focus', () => {
            if (!panelState.useCyclicApi) {
                dom.apiKeyInput.type = 'text';
                dom.apiKeyInput.value = realApiKey;
            }
        });

        dom.apiKeyInput.addEventListener('blur', () => {
            if (!panelState.useCyclicApi) {
                const newKey = dom.apiKeyInput.value.trim();
                localStorage.setItem(STORAGE_KEYS.SINGLE_API_KEY, newKey);
                realApiKey = newKey;
                updateApiKeyUI(dom.apiKeyInput);
            }
        });

        dom.btnManageApiKeys.addEventListener('click', () => {
            dom.apiKeyKeysTextarea.value = apiKeysList.join('\n');
            dom.apiKeyListModal.style.display = 'flex';
        });

        dom.btnSaveApiKeys.addEventListener('click', () => {
            const keysString = dom.apiKeyKeysTextarea.value;
            localStorage.setItem(STORAGE_KEYS.API_KEY_LIST, keysString);
            apiKeysList = keysString.split('\n').map(k => k.trim()).filter(Boolean);
            if (panelState.currentApiKeyIndex >= apiKeysList.length) {
                panelState.currentApiKeyIndex = 0;
            }
            saveState();
            updateApiKeyUI(dom.apiKeyInput);
            dom.apiKeyListModal.style.display = 'none';
        });

        dom.btnCancelApiKeys.addEventListener('click', () => {
            dom.apiKeyListModal.style.display = 'none';
        });

        dom.toggleCyclicApi.addEventListener('change', () => {
            panelState.useCyclicApi = dom.toggleCyclicApi.checked;
             if (panelState.useCyclicApi && apiKeysList.length === 0) {
                 alert('No API keys found. Add keys via "Manage Keys" to use cyclic mode.');
                 panelState.useCyclicApi = dom.toggleCyclicApi.checked = false;
            }
            saveState();
            updateApiKeyUI(dom.apiKeyInput);
        });

        dom.apiVersionSelect.addEventListener('change', () => {
            panelState.apiVersion = dom.apiVersionSelect.value;
            saveState();
        });

        dom.modelSelect.addEventListener('change', () => {
            panelState.currentModel = dom.modelSelect.value;
            panelState.currentPreset = null; // Deselect preset
            dom.presetSelect.value = '';
            updateCustomModelInputVisibility(dom.modelSelect, dom.customModelInput);
            loadModelSettings(panelState.currentModel, dom);
            saveState();
        });

        dom.presetSelect.addEventListener('change', () => {
            const presetName = dom.presetSelect.value;
            if (presetName) {
                const preset = allSettings.presets.find(p => p.name === presetName);
                if (preset) {
                    loadPreset(preset, dom);
                }
            } else {
                panelState.currentPreset = null;
                saveState();
            }
        });

        dom.btnGetModels.addEventListener('click', () => fetchModelsFromApi(dom));
        dom.btnSaveSettings.addEventListener('click', () => saveCurrentModelSettings(dom));

        dom.btnToggleThinkingParams.addEventListener('click', () => {
            panelState.thinkingParamsCollapsed = !panelState.thinkingParamsCollapsed;
            updateThinkingParamsVisibility(dom.thinkingModeParamsDiv, dom.btnToggleThinkingParams);
            saveState();
        });

        dom.toggleOverrideThinkingBudget.addEventListener('change', () => updateThinkingControlsState(dom));

        Object.values(dom.elems).forEach(({ num, range }) => linkInputs(num, range));

        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.');
                    return;
                }
                const settings = getSettingsFromForm(dom);
                const preset = { name, model: panelState.currentModel, settings };
                allSettings.presets.push(preset);
                panelState.currentPreset = name;
                saveState();
                fillPresetSelect(dom.presetSelect);
            }
        };

        btnDeletePreset.onclick = () => {
            const name = dom.presetSelect.value;
            if (name && confirm(`Delete preset "${name}"?`)) {
                allSettings.presets = allSettings.presets.filter(p => p.name !== name);
                if (panelState.currentPreset === name) {
                    panelState.currentPreset = null;
                }
                saveState();
                fillPresetSelect(dom.presetSelect);
            }
        };

        btnResetSettings.onclick = () => {
            applySettingsToForm(MODEL_SETTINGS_DEFAULTS, dom);
            if (panelState.currentModel === 'custom') dom.customModelInput.value = '';

            allSettings[panelState.currentModel] = { ...MODEL_SETTINGS_DEFAULTS };
            if (panelState.currentModel === 'custom') allSettings.customModelString = '';

            panelState.currentPreset = null;
            fillPresetSelect(dom.presetSelect);
            updateThinkingControlsState(dom);
            saveState();
            showSaveToast(dom.saveToast);
        };

        btnExportSettings.onclick = () => {
            const exportData = {
                settings: allSettings,
                panelState: panelState,
                singleApiKey: localStorage.getItem(STORAGE_KEYS.SINGLE_API_KEY),
                apiKeysList: localStorage.getItem(STORAGE_KEYS.API_KEY_LIST)
            };
            const blob = new Blob([JSON.stringify(exportData, null, 2)], { 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 = () => dom.inputImportSettings.click();

        inputImportSettings.onchange = (event) => {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const data = JSON.parse(e.target.result);
                    if (data.settings) allSettings = data.settings;
                    if (data.panelState) panelState = { ...panelState, ...data.panelState };
                    if (data.singleApiKey !== undefined) localStorage.setItem(STORAGE_KEYS.SINGLE_API_KEY, data.singleApiKey);
                    if (data.apiKeysList !== undefined) localStorage.setItem(STORAGE_KEYS.API_KEY_LIST, data.apiKeysList);

                    loadState(); // Reload all state from imported data
                    setupInitialUI(dom); // Re-initialize the entire UI

                    alert('Settings imported successfully.');
                } catch (err) {
                    alert('Error importing settings: ' + err.message);
                    console.error('Import error:', err);
                }
            };
            reader.readAsText(file);
            event.target.value = ''; // Reset file input
        };

    }

    // --- HTML & CSS Generators ---

    function buildPanelHTML() {
        return `
            <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
                </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: New features, might be unstable. v1: Stable.">?</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>
                <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 custom model" style="display:none;" />
                        <button id="btn-toggle-thinking-params" title="Toggle Thinking Mode">🧠</button>
                    </div>
                </div>
                <div id="thinking-mode-params" style="display:none;">
                    <label class="toggle-switch-label">
                        <input type="checkbox" id="toggle-overrideThinkingBudget" />
                        <span class="slider round"></span>
                        Override Thinking
                        <span class="tooltip" title="Works with Gemini 2.5 Pro/Flash models.">?</span>
                    </label>
                    <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="Computational budget. -1: auto, 0: disable.">?</span>
                            </div>
                        </label>
                        <input type="range" id="range-thinkingBudget" min="-1" max="32768" step="1" />
                    </div>
                    <label class="toggle-switch-label" id="include-thoughts-label">
                        <input type="checkbox" id="toggle-includeThoughts" />
                        <span class="slider round"></span>
                        Include Thoughts
                        <span class="tooltip" title="Include model's internal thought process in response.">?</span>
                    </label>
                </div>
                <!-- Parameter Sliders -->
                ${['temperature', 'maxTokens', 'topP', 'topK', 'candidateCount', 'frequencyPenalty', 'presencePenalty'].map(param => `
                <div class="param-group">
                    <label>${param.charAt(0).toUpperCase() + param.slice(1).replace('Tokens', ' Output Tokens')}:
                        <div class="input-container">
                            <input type="number" id="param-${param}" />
                            <span class="tooltip" title="Tooltip for ${param}">?</span>
                        </div>
                    </label>
                    <input type="range" id="range-${param}" />
                </div>
                `).join('')}
                 <div class="param-group">
                    <label>Safety Settings:
                        <div class="input-container">
                            <select id="safety-settings-select">
                            ${SAFETY_SETTINGS_OPTIONS.map(opt => `<option value="${opt.value}">${opt.name}</option>`).join('')}
                            </select>
                            <span class="tooltip" title="Adjusts safety filtering threshold.">?</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</button>
                <button id="btn-import-settings">Import</button>
                <input type="file" id="input-import-settings" style="display:none;" accept=".json" />
                <div id="save-toast">Settings saved!</div>
            </div>`;
    }

    function buildApiKeyModalHTML() {
        return `
            <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>`;
    }

    function queryDOMElements(panel, modal) {
        const dom = {
            panel,
            apiKeyListModal: modal,
            toggleBtn: panel.querySelector('.toggle-button'),
            apiKeyInput: panel.querySelector('#api-key-input'),
            btnManageApiKeys: panel.querySelector('#btn-manage-api-keys'),
            toggleCyclicApi: panel.querySelector('#toggle-cyclic-api'),
            apiVersionSelect: panel.querySelector('#apiVersion-select'),
            btnGetModels: panel.querySelector('#btn-get-models'),
            presetSelect: panel.querySelector('#preset-select'),
            btnAddPreset: panel.querySelector('#btn-add-preset'),
            btnDeletePreset: panel.querySelector('#btn-delete-preset'),
            modelSelect: panel.querySelector('#model-select'),
            customModelInput: panel.querySelector('#custom-model-input'),
            btnToggleThinkingParams: panel.querySelector('#btn-toggle-thinking-params'),
            thinkingModeParamsDiv: panel.querySelector('#thinking-mode-params'),
            toggleOverrideThinkingBudget: panel.querySelector('#toggle-overrideThinkingBudget'),
            thinkingBudgetGroup: panel.querySelector('#thinking-budget-group'),
            includeThoughtsLabel: panel.querySelector('#include-thoughts-label'),
            toggleIncludeThoughts: panel.querySelector('#toggle-includeThoughts'),
            btnSaveSettings: panel.querySelector('#btn-save-settings'),
            btnResetSettings: panel.querySelector('#btn-reset-settings'),
            btnExportSettings: panel.querySelector('#btn-export-settings'),
            inputImportSettings: panel.querySelector('#input-import-settings'),
            btnImportSettings: panel.querySelector('#btn-import-settings'),
            saveToast: panel.querySelector('#save-toast'),
            safetySettingsSelect: panel.querySelector('#safety-settings-select'),
            apiKeyKeysTextarea: modal.querySelector('#api-keys-textarea'),
            btnSaveApiKeys: modal.querySelector('#btn-save-api-keys'),
            btnCancelApiKeys: modal.querySelector('#btn-cancel-api-keys'),
            elems: {}
        };

        const paramNames = ['temperature', 'maxTokens', 'topP', 'topK', 'candidateCount', 'frequencyPenalty', 'presencePenalty', 'thinkingBudget'];
        paramNames.forEach(name => {
            dom.elems[name] = {
                num: panel.querySelector(`#param-${name}`),
                range: panel.querySelector(`#range-${name}`)
            };
        });

        // Set attributes for range inputs dynamically
        const ranges = {
            temperature: { min: 0, max: 2, step: 0.01 }, maxTokens: { min: 1, max: 65536, step: 1 },
            topP: { min: 0, max: 1, step: 0.01 }, topK: { min: 0, max: 1000, step: 1 },
            candidateCount: { min: 1, max: 8, step: 1 }, frequencyPenalty: { min: -2, max: 2, step: 0.01 },
            presencePenalty: { min: -2, max: 2, step: 0.01 }, thinkingBudget: { min: -1, max: 32768, step: 1 }
        };

        for (const [name, attrs] of Object.entries(ranges)) {
            const el = dom.elems[name];
            Object.assign(el.num, attrs);
            Object.assign(el.range, attrs);
        }

        return dom;
    }


    function getPanelStyle() {
        return `
        :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; 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; display: flex;
        }
        #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;
        }
        #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; }
        .model-input-container #model-select, .model-input-container #custom-model-input { flex-grow: 1; min-width: 0; }
        .model-input-container #btn-toggle-thinking-params { flex-shrink: 0; width: auto; margin: 0; padding: calc(min(0.6vw, 3px) * var(--scale-factor)) calc(min(1vw, 6px) * var(--scale-factor)); line-height: 1; 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, .param-group .input-container select { flex-grow: 1; min-width: 0; }
        .tooltip { flex-shrink: 0; cursor: help; color: #aaa; font-size: calc(min(2vw, 12px) * var(--scale-factor)); }
        .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%; }
        .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%; }
        #gemini-settings-panel button {
            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)); }
        #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: calc(min(1vw, 5px) * var(--scale-factor)) 0; background: rgba(40, 40, 40, 0.7); }
        .param-group.disabled, .toggle-switch-label.disabled { opacity: 0.5; pointer-events: none; }
        #gemini-settings-panel button: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; }
        #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; }
        .toggle-switch-label { position: relative; display: flex; align-items: center; width: 100%; margin: calc(min(0.8vw, 3px) * var(--scale-factor)) 0; font-size: calc(min(2.2vw, 12px) * var(--scale-factor)); padding-left: calc(min(6vw, 35px) * var(--scale-factor)); cursor: pointer; 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; 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; transition: .4s; border-radius: 50%; }
        input:checked + .slider { background-color: #4caf50; }
        input:checked + .slider:before { transform: translateX(calc(min(2.5vw, 14px) * var(--scale-factor))); }
        #api-key-list-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10001; }
        #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; min-height: calc(150px * var(--scale-factor)); 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; box-sizing: border-box; }
        #api-key-list-modal .modal-buttons { display: flex; justify-content: flex-end; gap: 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; }
        #btn-save-api-keys { background: #4caf50; } #btn-save-api-keys:hover { background: #388e3c; }
        #btn-cancel-api-keys { background: #f44336; } #btn-cancel-api-keys:hover { background: #d32f2f; }
        .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; scrollbar-width: thin; scrollbar-color: #888 #333; }
        .panel-content::-webkit-scrollbar { width: 8px; }
        .panel-content::-webkit-scrollbar-track { background: #333; border-radius: 4px; }
        .panel-content::-webkit-scrollbar-thumb { background-color: #888; border-radius: 4px; }
        .panel-content::-webkit-scrollbar-thumb:hover { background-color: #aaa; }
        `;
    }

    // --- Entry Point ---
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        initializePanel();
    } else {
        document.addEventListener('DOMContentLoaded', initializePanel);
    }
})();