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.6
// @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 = {}; // { modelName: {temperature, maxOutputTokens, topP, topK} }
    let panelState = {collapsed: true, currentModel: DEFAULT_MODEL};
    let modelList = []; // список строк моделей типа 'gemini-2.5-flash'
    let apiKey = '';

    // --- Создание панели ---
    function createPanel() {
        // Создаём корневой div
        const panel = document.createElement('div');
        panel.id = 'gemini-settings-panel';
        if(panelState.collapsed) panel.classList.add('collapsed');

        panel.innerHTML = `
            <div class="toggle-button" title="Показать/Скрыть панель">&#9654;</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>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:60px;" /></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:60px;" /></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:60px;" /></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:60px;" /></label>
                <input type="range" id="range-topK" min="0" max="100" step="1" />
            </div>
            <button id="btn-save-settings">Save Settings</button>
            <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 {
                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 {
                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);
            }
        `;
        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 modelSelect = panel.querySelector('#model-select');
        const customModelInput = panel.querySelector('#custom-model-input');
        const btnSaveSettings = panel.querySelector('#btn-save-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') }
        };

        // --- Восстановление API ключа из localStorage с маскировкой ---
        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; // чтобы хранить настоящий ключ, а в input маска
        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 = '';
            // Всегда добавляем custom как первый вариант
            const optCustom = document.createElement('option');
            optCustom.value = 'custom';
            optCustom.textContent = 'Custom';
            modelSelect.appendChild(optCustom);

            // Добавляем модели из modelList
            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') {
                // Если есть сохранённая модель у custom - подставим
                if(allSettings.customModelString) {
                    customModelInput.value = allSettings.customModelString;
                } else {
                    customModelInput.value = '';
                }
            }
        }

        // --- Сохранение параметров выбранной модели ---
        function saveModelSettings(model) {
            if (!model) model = DEFAULT_MODEL;
            if (!allSettings) allSettings = {};
            allSettings[model] = {
                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)
            };
            if(model === 'custom') {
                allSettings.customModelString = customModelInput.value.trim();
            }
            saveAllSettings();
        }

        // --- Ограничение значений ---
        function clamp(val, min, max) {
            if (isNaN(val)) return min;
            return Math.min(max, Math.max(min, val));
        }

        // --- Синхронизация range и number input ---
        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;
            });
        }

        // --- Сохраняем все настройки в localStorage ---
        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);
            }
        }

        // --- Загружаем все настройки из localStorage ---
        // --- Загрузка всех настроек ---
        function loadAllSettings() {
            try {
                const s = localStorage.getItem(STORAGE_SETTINGS_KEY);
                if (s) {
                    allSettings = JSON.parse(s);
                } else {
                    allSettings = {};
                }
            } catch(e) {
                console.error('Ошибка загрузки настроек:', e);
                allSettings = {};
            }

            // Восстановим modelList если был
            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);
        }

        // --- Получение моделей с API ---
        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();
                /*
                Пример структуры data:
                {
                  "models": [
                    {"name": "models/gemini-2.0-flash", ...},
                    ...
                  ]
                }
                */
                // Парсим список моделей с префиксом "models/gemini-"
                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();
                // Если есть сохранённая модель, выбираем её, иначе default
                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';
            }
        }

        // --- Подменяем модель в URL ---
        function replaceModelInUrl(url, modelName) {
            if(typeof url !== 'string') return url;
            if(modelName === 'custom') {
                modelName = allSettings.customModelString || '';
                if(!modelName) return url; // нет кастомной модели
            }
            // модель всегда окружена "models/" слева и ":" справа
            // нужно заменить только часть между "models/" и ":"
            // например: https://.../models/gemini-2.5-flash-preview-04-17:generateText
            return url.replace(/(models\/)([^:]+)(:)/, (m, p1, p2, p3) => p1 + modelName + p3);
        }

        // --- Обработка fetch ---
        const originalFetch = window.fetch;
        window.fetch = async function(input, init) {
            let url = input;

            // Меняем модель в URL, если есть ключ API и url с generativelanguage
            if(typeof url === 'string' && url.includes('generativelanguage.googleapis.com')) {
                url = replaceModelInUrl(url, panelState.currentModel);
            }

            // Проверяем тело запроса с generationConfig
            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);
            saveAllSettings();
        };

        // Кастомная модель input меняется - сохраняем
        customModelInput.oninput = () => {
            saveModelSettings('custom');
        };

        // Связываем range и number inputs
        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);

        // При изменении параметров вручную сохраняем
        for (const key in elems) {
            elems[key].num.oninput = () => saveModelSettings(panelState.currentModel);
            elems[key].range.oninput = () => saveModelSettings(panelState.currentModel);
        }

        // Кнопка получения списка моделей
        btnGetModels.onclick = fetchModelsFromApi;

        // Кнопка сохранения параметров
        btnSaveSettings.onclick = () => {
            saveModelSettings(panelState.currentModel);
            saveAllSettings();
        };

        // --- Инициализация при загрузке ---
        loadPanelState();
        loadAllSettings();
        loadApiKey();

        // Заполняем селект, если уже были модели из localStorage
        if(allSettings.modelList) {
            modelList = allSettings.modelList;
        }

        fillModelSelect();

        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();

})();