Google AI Studio easy use

Automatically set Google AI Studio system prompt; Increase chat content font size; Toggle Grounding with Ctrl/Cmd + i. 自动设置 Google AI Studio 的系统提示词;增大聊天内容字号;快捷键 Ctrl/Cmd + i 开关Grounding。

/*
 * File: ai-studio-easy-use.js
 * Project: browser-scipts
 * Created: 2025-03-03 10:46:13
 * Author: Victor Cheng
 * Email: [email protected]
 * Description:
 */

// ==UserScript==
// @name         Google AI Studio easy use
// @namespace    http://tampermonkey.net/
// @version      1.1.2
// @description  Automatically set Google AI Studio system prompt; Increase chat content font size; Toggle Grounding with Ctrl/Cmd + i. 自动设置 Google AI Studio 的系统提示词;增大聊天内容字号;快捷键 Ctrl/Cmd + i 开关Grounding。
// @author       Victor Cheng
// @match        https://aistudio.google.com/*
// @match        https://ai.dev/*
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    //=======================================
    // 常量管理
    //=======================================
    const CONSTANTS = {
        STORAGE_KEYS: {
            SYSTEM_PROMPT: 'aiStudioSystemPrompt',
            FONT_SIZE: 'aiStudioFontSize'
        },
        DEFAULTS: {
            SYSTEM_PROMPT: '1. Answer in the same language as the question.\n2. If web search is necessary, always search in English.',
            FONT_SIZE: 'medium'
        },
        SELECTORS: {
            NAVIGATION: '[role="navigation"]',
            SYSTEM_INSTRUCTIONS: '.toolbar-system-instructions',
            SYSTEM_INSTRUCTIONS_BUTTON: 'button[aria-label="System instructions"]',
            SYSTEM_TEXTAREA: '.toolbar-system-instructions textarea',
            NEW_CHAT_LINK: 'a[href$="/prompts/new_chat"]',
            SEARCH_TOGGLE: '.search-as-a-tool-toggle button',
            CHAT_LINKS: '.nav-sub-items-wrapper a'
        },
        FONT_SIZES: [
            { value: 'small', label: 'Small', size: '12px' },
            { value: 'medium', label: 'Medium', size: '14px' },
            { value: 'large', label: 'Large', size: '16px' },
            { value: 'x-large', label: 'X-large', size: '18px' },
            { value: 'xx-large', label: 'XX-large', size: '20px' }
        ],
        SHORTCUTS: {
            TOGGLE_GROUNDING: { key: 'i', requiresCmd: true },
            NEW_CHAT: { key: 'j', requiresCmd: true },
            SWITCH_CHAT: { key: '/', requiresCmd: true }
        }
    };

    //=======================================
    // 工具类
    //=======================================
    class DOMUtils {
        static createElement(tag, attributes = {}, styles = {}) {
            const element = document.createElement(tag);
            Object.entries(attributes).forEach(([key, value]) => {
                if (key === 'textContent') {
                    element.textContent = value;
                } else if (key === 'className') {
                    element.className = value;
                } else {
                    element.setAttribute(key, value);
                }
            });
            Object.assign(element.style, styles);
            return element;
        }

        static querySelector(selector) {
            return document.querySelector(selector);
        }

        static querySelectorAll(selector) {
            return document.querySelectorAll(selector);
        }
    }

    class StyleManager {
        static createStyleSheet(id, css) {
            let style = document.getElementById(id);
            if (!style) {
                style = DOMUtils.createElement('style', { id });
                document.head.appendChild(style);
            }
            style.textContent = css;
            return style;
        }

        static updateFontSize(size) {
            const fontSize = CONSTANTS.FONT_SIZES.find(s => s.value === size)?.size || '14px';
            this.createStyleSheet('aiStudioCustomStyle', `
                body:not(.dark-theme) ms-cmark-node p {
                    font-size: ${fontSize} !important;
                }
            `);
        }
    }

    class SystemPromptManager {
        static async update(prompt) {
            const systemInstructionsButton = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_INSTRUCTIONS_BUTTON);
            if (!systemInstructionsButton) {
                console.warn("System instructions button not found.");
                return false;
            }

            let textarea = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_TEXTAREA);
            if (!textarea) {
                systemInstructionsButton.click();
                await new Promise(resolve => setTimeout(resolve, 200));
                textarea = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_TEXTAREA);
            }

            if (textarea) {
                textarea.value = prompt;
                textarea.dispatchEvent(new Event('input', {
                    bubbles: true,
                    cancelable: true,
                }));
                return true;
            } else {
                console.warn("System prompt textarea not found after clicking button.");
                return false;
            }
        }
    }

    //=======================================
    // 功能类
    //=======================================
    class ShortcutManager {
        constructor() {
            this.currentChatIndex = 0;
            this.bindGlobalShortcuts();
        }

        bindGlobalShortcuts() {
            window.addEventListener('keydown', (e) => this.handleKeydown(e), {
                capture: true,
                passive: false
            });
        }

        handleKeydown(e) {
            const isCmdOrCtrl = e.metaKey || e.ctrlKey;
            if (!isCmdOrCtrl) return;

            const key = e.key.toLowerCase();
            const shortcut = Object.entries(CONSTANTS.SHORTCUTS)
                .find(([_, value]) => value.key === key && value.requiresCmd);

            if (!shortcut) return;

            e.preventDefault();
            e.stopPropagation();

            switch(shortcut[0]) {
                case 'TOGGLE_GROUNDING':
                    this.toggleGrounding();
                    break;
                case 'NEW_CHAT':
                    this.createNewChat();
                    break;
                case 'SWITCH_CHAT':
                    this.switchToNextChat();
                    break;
            }
        }

        toggleGrounding() {
            const searchToggle = DOMUtils.querySelector(CONSTANTS.SELECTORS.SEARCH_TOGGLE);
            searchToggle?.click();
        }

        createNewChat() {
            const newChatLink = DOMUtils.querySelector(CONSTANTS.SELECTORS.NEW_CHAT_LINK);
            if (newChatLink) {
                newChatLink.click();
                this.currentChatIndex = 0;
            }
        }

        switchToNextChat() {
            const chatLinks = DOMUtils.querySelectorAll(CONSTANTS.SELECTORS.CHAT_LINKS);
            if (chatLinks.length > 0) {
                chatLinks[this.currentChatIndex].click();
                this.currentChatIndex = (this.currentChatIndex + 1) % chatLinks.length;
            }
        }
    }

    //=======================================
    // UI相关类
    //=======================================
    class UIComponents {
        static createSettingLink() {
            return DOMUtils.createElement('a',
                { textContent: '⚙️ Easy use settings', className: 'easy-use-settings' },
                {
                    display: 'block',
                    color: '#076eff',
                    textDecoration: 'none',
                    fontSize: '14px',
                    marginBottom: '20px',
                    cursor: 'pointer'
                }
            );
        }

        static createShortcutsSection() {
            const shortcutsSection = DOMUtils.createElement('div', {}, {
                marginBottom: '24px',
                padding: '12px',
                background: '#f8f9fa',
                borderRadius: '4px'
            });

            const shortcutsTitle = DOMUtils.createElement('div',
                { textContent: 'Keyboard Shortcuts' },
                {
                    fontWeight: '500',
                    marginBottom: '8px',
                    color: '#202124'
                }
            );

            const shortcutsList = DOMUtils.createElement('div', {}, {
                fontSize: '14px',
                color: '#5f6368'
            });

            // 创建快捷键列表
            const shortcuts = [
                { key: 'Ctrl/Cmd + i', description: 'Toggle Grounding' },
                { key: 'Ctrl/Cmd + j', description: 'New Chat' },
                { key: 'Ctrl/Cmd + /', description: 'Switch Recent Chats' }
            ];

            shortcuts.forEach(({ key, description }) => {
                const shortcutItem = DOMUtils.createElement('div');
                shortcutItem.textContent = '• ';
                const kbd = DOMUtils.createElement('kbd', { textContent: key });
                const text = document.createTextNode(`: ${description}`);
                shortcutItem.appendChild(kbd);
                shortcutItem.appendChild(text);
                shortcutsList.appendChild(shortcutItem);
            });

            shortcutsSection.appendChild(shortcutsTitle);
            shortcutsSection.appendChild(shortcutsList);
            return shortcutsSection;
        }
    }

    class DialogManager {
        constructor(settingsManager) {
            this.settingsManager = settingsManager;
            this.dialog = null;
            this.overlay = null;
        }

        createOverlay() {
            return DOMUtils.createElement('div', {}, {
                position: 'fixed',
                top: '0',
                left: '0',
                width: '100%',
                height: '100%',
                background: 'rgba(0,0,0,0.5)',
                zIndex: '9999'
            });
        }

        createDialog() {
            const settings = this.settingsManager.getSettings();
            const dialog = DOMUtils.createElement('div', {}, {
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                background: 'white',
                padding: '30px',
                borderRadius: '8px',
                boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
                zIndex: '10000',
                minWidth: '450px',
                maxWidth: '700px',
                width: '50vw'
            });

            // 添加标题
            const title = DOMUtils.createElement('h2',
                { textContent: '⚙️ Easy Use Settings' },
                {
                    margin: '0 0 20px 0',
                    fontSize: '18px',
                    color: '#202124'
                }
            );
            dialog.appendChild(title);

            // 添加系统提示词设置
            const promptSection = this.createPromptSection(settings);
            dialog.appendChild(promptSection);

            // 添加字体大小设置
            const fontSection = this.createFontSection(settings);
            dialog.appendChild(fontSection);

            // 添加快捷键说明
            dialog.appendChild(UIComponents.createShortcutsSection());

            // 添加按钮
            const buttonContainer = this.createButtonContainer();
            dialog.appendChild(buttonContainer);

            return dialog;
        }

        createPromptSection(settings) {
            const section = DOMUtils.createElement('div', {}, {
                marginBottom: '24px'
            });

            const label = DOMUtils.createElement('label',
                { textContent: 'Global System Prompt' },
                {
                    display: 'block',
                    marginBottom: '8px',
                    fontWeight: '500',
                    color: '#202124'
                }
            );

            const textarea = document.createElement('textarea');
            textarea.value = settings.systemPrompt;
            Object.assign(textarea.style, {
                width: '100%',
                minHeight: '100px',
                marginBottom: '8px',
                padding: '8px',
                border: '1px solid #dadce0',
                borderRadius: '4px',
                fontFamily: 'inherit',
                resize: 'vertical'
            });
            textarea.spellcheck = false;

            const resetButton = DOMUtils.createElement('button',
                { textContent: 'Reset to Default' },
                {
                    padding: '4px 8px',
                    backgroundColor: '#f8f9fa',
                    color: '#3c4043',
                    border: '1px solid #dadce0',
                    borderRadius: '4px',
                    cursor: 'pointer',
                    fontSize: '12px',
                    marginBottom: '16px'
                }
            );

            resetButton.addEventListener('click', () => {
                textarea.value = CONSTANTS.DEFAULTS.SYSTEM_PROMPT;
            });

            section.appendChild(label);
            section.appendChild(textarea);
            section.appendChild(resetButton);
            return section;
        }

        createFontSection(settings) {
            const section = DOMUtils.createElement('div', {}, {
                marginBottom: '24px'
            });

            const label = DOMUtils.createElement('label',
                { textContent: 'Font Size' },
                {
                    display: 'block',
                    marginBottom: '8px',
                    fontWeight: '500',
                    color: '#202124'
                }
            );

            const buttonGroup = DOMUtils.createElement('div',
                { className: 'font-button-group' },
                {
                    display: 'flex',
                    gap: '8px',
                    width: '100%'
                }
            );

            CONSTANTS.FONT_SIZES.forEach(size => {
                const button = DOMUtils.createElement('button',
                    {
                        type: 'button',
                        value: size.value,
                        textContent: size.label,
                        title: `${size.label} (${size.size})`
                    },
                    {
                        ...this.getFontButtonStyles(size.value === settings.fontSize),
                        fontSize: size.size
                    }
                );

                if (size.value === settings.fontSize) {
                    button.setAttribute('data-selected', 'true');
                }

                button.addEventListener('click', () => this.handleFontButtonClick(button, buttonGroup));
                buttonGroup.appendChild(button);
            });

            section.appendChild(label);
            section.appendChild(buttonGroup);
            return section;
        }

        getFontButtonStyles(isSelected) {
            return {
                flex: '1',
                padding: '8px',
                border: `1px solid ${isSelected ? '#076eff' : '#dadce0'}`,
                borderRadius: '4px',
                background: isSelected ? '#e8f0fe' : 'white',
                color: isSelected ? '#076eff' : '#3c4043',
                cursor: 'pointer',
                transition: 'all 0.2s',
                fontFamily: 'inherit'
            };
        }

        handleFontButtonClick(clickedButton, buttonGroup) {
            buttonGroup.querySelectorAll('button').forEach(btn => {
                const isThisButton = btn === clickedButton;
                Object.assign(btn.style, {
                    ...this.getFontButtonStyles(isThisButton),
                    fontSize: CONSTANTS.FONT_SIZES.find(s => s.value === btn.value)?.size
                });
                if (isThisButton) {
                    btn.setAttribute('data-selected', 'true');
                } else {
                    btn.removeAttribute('data-selected');
                }
            });
        }

        createButtonContainer() {
            const container = DOMUtils.createElement('div', {
                className: 'dialog-buttons'
            }, {
                display: 'flex',
                gap: '10px',
                justifyContent: 'flex-end'
            });

            const saveButton = DOMUtils.createElement('button', {
                className: 'save-button',
                textContent: 'Save'
            }, {
                padding: '8px 16px',
                backgroundColor: '#076eff',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                fontWeight: '500'
            });

            const cancelButton = DOMUtils.createElement('button', {
                className: 'cancel-button',
                textContent: 'Cancel'
            }, {
                padding: '8px 16px',
                backgroundColor: '#f8f9fa',
                color: '#3c4043',
                border: '1px solid #dadce0',
                borderRadius: '4px',
                cursor: 'pointer',
                fontWeight: '500'
            });

            container.appendChild(cancelButton);
            container.appendChild(saveButton);
            return container;
        }

        show() {
            this.overlay = this.createOverlay();
            this.dialog = this.createDialog();
            document.body.appendChild(this.overlay);
            document.body.appendChild(this.dialog);
            this.bindEvents();
        }

        hide() {
            if (this.dialog && this.overlay) {
                document.body.removeChild(this.dialog);
                document.body.removeChild(this.overlay);
                this.dialog = null;
                this.overlay = null;
            }
        }

        bindEvents() {
            const saveButton = this.dialog.querySelector('.save-button');
            const cancelButton = this.dialog.querySelector('.cancel-button');

            if (saveButton && cancelButton) {
                saveButton.addEventListener('click', () => this.handleSave());
                cancelButton.addEventListener('click', () => this.hide());
            }
        }

        handleSave() {
            const textarea = this.dialog.querySelector('textarea');
            const selectedFontButton = this.dialog.querySelector('button[data-selected="true"]');

            if (!textarea || !selectedFontButton) return;

            const newSettings = {
                systemPrompt: textarea.value.trim(),
                fontSize: selectedFontButton.value
            };

            this.settingsManager.saveSettings(newSettings);
            StyleManager.updateFontSize(newSettings.fontSize);
            SystemPromptManager.update(newSettings.systemPrompt);
            this.hide();
        }
    }

    //=======================================
    // 核心管理器类
    //=======================================
    class SettingsManager {
        constructor() {
            this.settings = this.loadSettings();
        }

        loadSettings() {
            return {
                systemPrompt: localStorage.getItem(CONSTANTS.STORAGE_KEYS.SYSTEM_PROMPT) || CONSTANTS.DEFAULTS.SYSTEM_PROMPT,
                fontSize: localStorage.getItem(CONSTANTS.STORAGE_KEYS.FONT_SIZE) || CONSTANTS.DEFAULTS.FONT_SIZE
            };
        }

        saveSettings(settings) {
            localStorage.setItem(CONSTANTS.STORAGE_KEYS.SYSTEM_PROMPT, settings.systemPrompt);
            localStorage.setItem(CONSTANTS.STORAGE_KEYS.FONT_SIZE, settings.fontSize);
            this.settings = settings;
        }

        getSettings() {
            return { ...this.settings };
        }
    }

    class AppManager {
        constructor() {
            this.settingsManager = new SettingsManager();
            this.shortcutManager = new ShortcutManager();
            this.dialogManager = new DialogManager(this.settingsManager);
        }

        init() {
            this.initSettingsLink();
            this.applyInitialSettings();
            this.observeRouteChanges();
        }

        initSettingsLink() {
            const link = UIComponents.createSettingLink();
            link.addEventListener('click', () => this.dialogManager.show());

            this.observeNavigation(link);
        }

        observeNavigation(link) {
            const observer = new MutationObserver((_, obs) => {
                const nav = DOMUtils.querySelector(CONSTANTS.SELECTORS.NAVIGATION);
                if (nav && !nav.querySelector('.easy-use-settings')) {
                    link.classList.add('easy-use-settings');
                    nav.insertBefore(link, nav.firstChild);
                    obs.disconnect();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        applyInitialSettings() {
            const settings = this.settingsManager.getSettings();
            StyleManager.updateFontSize(settings.fontSize);
            this.initSystemPrompt(settings.systemPrompt);
        }

        async initSystemPrompt(prompt, maxRetries = 10, interval = 1000) {
            const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));

            for (let i = 0; i < maxRetries; i++) {
                if (document.readyState !== 'complete') {
                    await wait(interval);
                    continue;
                }

                const success = await SystemPromptManager.update(prompt);
                if (success) {
                    console.log("System prompt updated successfully.");
                    return;
                }

                console.log(`Attempt ${i + 1} to set system prompt failed. Retrying...`);
                await wait(interval);
            }

            console.error(`Failed to set system prompt after ${maxRetries} attempts.`);
        }

        observeRouteChanges() {
            let lastUrl = location.href;
            const observer = new MutationObserver(() => {
                const url = location.href;
                if (url !== lastUrl) {
                    lastUrl = url;
                    this.applyInitialSettings();
                }
            });

            observer.observe(document, {
                subtree: true,
                childList: true
            });
        }
    }

    // 启动应用
    new AppManager().init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址