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。

目前為 2025-02-26 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/*
 * File: global-system-prompt.js
 * Project: g-ai-studio-global-system-prompt
 * Created: 2025-01-10 09:46:13
 * Author: Victor Cheng
 * Email: [email protected]
 * Description:
 */

// ==UserScript==
// @name         Google AI Studio easy use
// @namespace    http://tampermonkey.net/
// @version      1.0.9
// @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/*
// @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: '.system-instructions',
            SYSTEM_TEXTAREA: '.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 update(prompt) {
            const systemInstructions = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_INSTRUCTIONS);
            const textarea = systemInstructions?.querySelector('textarea');
            if (textarea) {
                textarea.value = prompt;
                textarea.dispatchEvent(new Event('input', {
                    bubbles: true,
                    cancelable: true,
                }));
            }
        }
    }

    //=======================================
    // 功能类
    //=======================================
    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 systemInstructions = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_INSTRUCTIONS);
                const textarea = systemInstructions?.querySelector('textarea');

                if (textarea?.spellcheck === true) {
                    SystemPromptManager.update(prompt);
                    return;
                }

                await wait(interval);
            }
        }

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