Chub AI palm2 model list Enhancer

Панель настройки Gemini: выбор модели из API, параметры, пресеты, сброс настроек, подсказки, экспорт/импорт настроек

当前为 2025-06-21 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Chub AI palm2 model list Enhancer
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  Панель настройки Gemini: выбор модели из API, параметры, пресеты, сброс настроек, подсказки, экспорт/импорт настроек
// @author       Ko16aska
// @match        *://chub.ai/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- Ключи localStorage ---
    const STORAGE_SETTINGS_KEY = 'chubGeminiSettings';
    const STORAGE_PANEL_STATE_KEY = 'chubGeminiPanelState';
    const STORAGE_API_KEY = 'chubGeminiApiKey';

    // --- По умолчанию ---
    const DEFAULT_MODEL = 'custom';
    const API_MODELS_URL_BASE = 'https://generativelanguage.googleapis.com/v1beta/models?key=';

    // --- Переменные состояния ---
    let allSettings = {};
    let panelState = {collapsed: true, currentModel: DEFAULT_MODEL, currentPreset: null};
    let modelList = [];
    let apiKey = '';

    // --- Создание панели ---
    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="Показать/Скрыть панель">▶</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" style="margin-bottom:12px;">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="Введите свою модель" style="display:none; margin-top:4px; width:100%;" />
            </label>
            <div class="param-group">
                <label>Temperature: <input type="number" step="0.01" id="param-temperature" style="width:80px;" /> <span class="tooltip" title="Контролирует случайность вывода. Более высокие значения делают вывод более случайным, а низкие — более детерминированным.">?</span></label>
                <input type="range" id="range-temperature" min="0" max="2" step="0.01" />
            </div>
            <div class="param-group">
                <label>MaxOutputTokens: <input type="number" step="1" id="param-maxTokens" style="width:80px;" /> <span class="tooltip" title="Максимальное количество токенов для генерации.">?</span></label>
                <input type="range" id="range-maxTokens" min="1" max="65536" step="1" />
            </div>
            <div class="param-group">
                <label>topP: <input type="number" step="0.01" id="param-topP" style="width:80px;" /> <span class="tooltip" title="Параметр выборки ядра. Модель рассматривает минимальный набор токенов, чья суммарная вероятность превышает topP.">?</span></label>
                <input type="range" id="range-topP" min="0" max="1" step="0.01" />
            </div>
            <div class="param-group">
                <label>topK: <input type="number" step="1" id="param-topK" style="width:80px;" /> <span class="tooltip" title="Модель учитывает только K токенов с наибольшей вероятностью.">?</span></label>
                <input type="range" id="range-topK" min="0" max="1000" step="1" />
            </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 = `
            #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: 1px solid #444;
                border-radius: 8px 0 0 8px;
                padding: 12px 16px;
                width: 300px;
                box-shadow: 0 4px 16px rgba(0,0,0,0.7);
                font-family: Arial, sans-serif;
                font-size: 14px;
                z-index: 10000;
                transition: transform 0.4s ease;
                user-select: none;
            }
            #gemini-settings-panel:not(.collapsed) {
                transform: translateY(-50%) translateX(0);
            }
            #gemini-settings-panel h4 {
                text-align: center;
                margin: 0 0 10px;
            }
            #gemini-settings-panel label {
                display: block;
                margin-bottom: 8px;
                font-weight: 600;
            }
            #gemini-settings-panel input[type="number"],
            #gemini-settings-panel input[type="text"],
            #gemini-settings-panel input[type="password"],
            #gemini-settings-panel select {
                margin-left: 8px;
                background: #222;
                border: 1px solid #555;
                border-radius: 4px;
                color: #eee;
                padding: 2px 6px;
                font-size: 13px;
            }
            #gemini-settings-panel input[type="password"] {
                letter-spacing: 0.3em;
            }
            .param-group {
                margin-bottom: 12px;
            }
            .param-group input[type="range"] {
                width: 100%;
                margin-top: 4px;
                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: 7px;
                border: none;
                border-radius: 5px;
                background: #4caf50;
                color: #fff;
                font-weight: 600;
                cursor: pointer;
                user-select: none;
                margin-top: 6px;
                transition: background-color 0.3s ease;
            }
            #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: 10px;
                text-align: center;
                background: #222;
                color: #0f0;
                padding: 6px;
                border-radius: 5px;
                opacity: 0;
                transition: opacity 0.5s ease;
                pointer-events: none;
                user-select: none;
            }
            #save-toast.show {
                opacity: 1;
            }
            .toggle-button {
                position: absolute;
                left: -28px;
                top: 50%;
                transform: translateY(-50%);
                width: 28px;
                height: 48px;
                background: rgba(30,30,30,0.85);
                border: 1px solid #444;
                border-radius: 8px 0 0 8px;
                color: #eee;
                text-align: center;
                line-height: 48px;
                font-size: 20px;
                cursor: pointer;
                user-select: none;
                transition: transform 0.3s ease;
            }
            #gemini-settings-panel.collapsed .toggle-button {
                transform: translateY(-50%) rotate(0deg);
            }
            #gemini-settings-panel:not(.collapsed) .toggle-button {
                transform: translateY(-50%) rotate(0deg);
            }
            .tooltip {
                cursor: help;
                color: #aaa;
                font-size: 12px;
                margin-left: 4px;
            }
        `;
        document.head.appendChild(style);

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

        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: 1.0,
                maxOutputTokens: 2048,
                topP: 0.95,
                topK: 0
            };
            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;
            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), 1, 65536),
                topP: clamp(parseFloat(elems.topP.num.value), 0, 1),
                topK: clamp(parseInt(elems.topK.num.value), 0, 1000),
                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
            };
            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('Ошибка сохранения настроек:', e);
            }
        }

        function loadAllSettings() {
            try {
                const s = localStorage.getItem(STORAGE_SETTINGS_KEY);
                if (s) allSettings = JSON.parse(s);
                else allSettings = {};
            } catch(e) {
                console.error('Ошибка загрузки настроек:', 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('Ошибка загрузки состояния панели:', 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('Insert API key here');
                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-'))
                        .map(name => name.substring('models/'.length));
                } 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('Ошибка загрузки моделей: ' + 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;
            }
            return url.replace(/(models\/)([^:]+)(:)/, (m, p1, p2, p3) => p1 + modelName + p3);
        }

        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"')) {
                try {
                    const requestBody = JSON.parse(init.body);
                    if(requestBody.generationConfig) {
                        const s = allSettings[panelState.currentModel] || {
                            temperature: 2,
                            maxOutputTokens: 65536,
                            topP: 0.95,
                            topK: 0
                        };
                        requestBody.generationConfig.temperature = s.temperature;
                        requestBody.generationConfig.maxOutputTokens = s.maxOutputTokens;
                        requestBody.generationConfig.topP = s.topP;
                        requestBody.generationConfig.topK = s.topK;
                        init.body = JSON.stringify(requestBody);
                    }
                } catch(e) {
                    console.error('Ошибка обработки тела запроса:', 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, 1, 65536, 1);
        linkInputs(elems.topP.num, elems.topP.range, 0, 1, 0.01);
        linkInputs(elems.topK.num, elems.topK.range, 0, 1000, 1);

        btnGetModels.onclick = fetchModelsFromApi;

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

        // Управление пресетами
        function fillPresetSelect() {
            presetSelect.innerHTML = '';
            const opt = document.createElement('option');
            opt.value = '';
            opt.textContent = 'Выберите пресет';
            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);
                }
            }
        }

        // Обновленная функция loadPreset
        function loadPreset(preset) {
            const model = preset.model;
            allSettings[model] = {
                temperature: preset.settings.temperature,
                maxOutputTokens: preset.settings.maxOutputTokens,
                topP: preset.settings.topP,
                topK: preset.settings.topK
            };
            if (model === 'custom') {
                allSettings.customModelString = preset.settings.customModelString || '';
            }
            panelState.currentModel = model;
            panelState.currentPreset = preset.name;
            modelSelect.value = model;
            updateCustomModelInputVisibility();
            loadModelSettings(model);
        }

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

        btnAddPreset.onclick = () => {
            const name = prompt('Введите название пресета:');
            if (name) {
                const settings = getCurrentSettings();
                const preset = {
                    name,
                    model: panelState.currentModel,
                    settings
                };
                if (!allSettings.presets) allSettings.presets = [];
                allSettings.presets.push(preset);
                saveAllSettings();
                fillPresetSelect();
            }
        };

        btnDeletePreset.onclick = () => {
            const name = presetSelect.value;
            if (name) {
                allSettings.presets = allSettings.presets.filter(p => p.name !== name);
                saveAllSettings();
                fillPresetSelect();
            }
        };

        btnResetSettings.onclick = () => {
            const defaultSettings = {
                temperature: 2.0,
                maxOutputTokens: 65536,
                topP: 0.95,
                topK: 0
            };
            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;
            if (panelState.currentModel === 'custom') {
                customModelInput.value = '';
            }
            saveModelSettings(panelState.currentModel);
        };

        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);
                        allSettings = json;
                        saveAllSettings();
                        fillModelSelect();
                        fillPresetSelect();
                        loadModelSettings(panelState.currentModel);
                        alert('Настройки успешно импортированы');
                    } catch (err) {
                        alert('Ошибка импорта настроек: ' + err.message);
                    }
                };
                reader.readAsText(file);
            }
        };

        loadPanelState();
        loadAllSettings();
        loadApiKey();
        if(allSettings.modelList) {
            modelList = allSettings.modelList;
        }
        fillModelSelect();
        fillPresetSelect();
        if(panelState.currentModel && modelList.includes(panelState.currentModel)) {
            modelSelect.value = 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');
        }
    }

    createPanel();
})();