Google AI Studio Helper

整合了“从此处删除”、“HTML预览(已修复)”和“自动中文输出”三大功能,全面提升 Google AI Studio 使用体验。

// ==UserScript==
// @name         Google AI Studio Helper
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  整合了“从此处删除”、“HTML预览(已修复)”和“自动中文输出”三大功能,全面提升 Google AI Studio 使用体验。
// @author       Chris_C
// @match        *://aistudio.google.com/*
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com
// @license MIT
// ==/UserScript==


/**
 * 模块一:自动添加“中文输出”指令
 */
(function() {
    'use strict';

    // --- 配置常量 ---
    // 聊天回合元素的选择器
    const CHAT_TURN_SELECTOR = 'ms-chat-turn';
    // 选项按钮的选择器(支持中英文)
    const OPTIONS_BUTTON_SELECTOR = 'button[aria-label="Open options"], button[aria-label="打开选项"]';
    // 聊天会话容器的选择器
    const CHAT_SESSION_SELECTOR = 'ms-chat-session';
    // --- 配置结束 ---

    console.log('AI Studio Delete Script: Initializing (v2.0)');

    /**
     * 快速删除函数
     * @param {Element} turnElement - 要删除的回合元素
     * @returns {Promise<boolean>} 删除是否成功
     */
    async function directDelete(turnElement) {
        if (!turnElement) {
            const turns = document.querySelectorAll(CHAT_TURN_SELECTOR);
            turnElement = turns[turns.length - 1];
        }
        
        if (!turnElement) return false;
        
        const optionsBtn = turnElement.querySelector('button[aria-label="Open options"]');
        if (!optionsBtn) return false;
        
        return new Promise(resolve => {
            // 监听菜单出现
            const observer = new MutationObserver(() => {
                const menu = document.querySelector('.cdk-overlay-pane');
                if (menu) {
                    const deleteBtn = Array.from(menu.querySelectorAll('button'))
                        .find(btn => btn.textContent?.toLowerCase().includes('delete'));
                    
                    if (deleteBtn) {
                        deleteBtn.click();
                        observer.disconnect();
                        // 快速检查删除结果
                        setTimeout(() => resolve(!document.contains(turnElement)), 200);
                    }
                }
            });
            
            observer.observe(document.body, {childList: true, subtree: true});
            optionsBtn.click();
            
            // 超时处理
            setTimeout(() => {observer.disconnect(); resolve(false);}, 1000);
        });
    }

    /**
     * 执行删除操作的异步函数(原始方法作为备用)
     * @param {Element} turnElement - 要删除的对话回合元素
     * @returns {Promise<Object>} 返回删除操作的结果
     */
    async function performDeleteAction(turnElement) {
        return new Promise(resolve => {
            // 查找当前回合的选项按钮
            const optionsButton = turnElement.querySelector(OPTIONS_BUTTON_SELECTOR);
            if (!optionsButton) {
                console.warn('Delete action failed: Options button not found on a turn.', turnElement);
                resolve({ success: false });
                return;
            }

            // 创建观察器来监听菜单的出现
            const menuObserver = new MutationObserver((mutations, observer) => {
                // 查找弹出的菜单面板
                const menuPanel = document.querySelector('.cdk-overlay-pane, [role="menu"]');
                if (menuPanel) {
                    // 等待菜单内容完全加载
                    setTimeout(() => {
                        observer.disconnect(); // 停止观察
                    
                    // 获取菜单中的所有可点击元素
                    const allButtons = Array.from(menuPanel.querySelectorAll('button, [role="menuitem"], .mat-menu-item'));
                    console.log('Found menu items:', allButtons.map(btn => ({
                        text: btn.textContent?.trim(),
                        ariaLabel: btn.getAttribute('aria-label'),
                        className: btn.className
                    })));
                    
                    // 查找删除按钮(支持中英文,更宽泛的匹配)
                    const deleteButton = allButtons.find(btn => {
                        const text = (btn.textContent || '').trim().toLowerCase();
                        const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
                        const title = (btn.getAttribute('title') || '').toLowerCase();
                        
                        // 检查各种可能的删除相关文本
                        const deleteKeywords = ['delete', '删除', 'remove', '移除', 'trash', '垃圾', 'del'];
                        return deleteKeywords.some(keyword => 
                            text.includes(keyword) || 
                            ariaLabel.includes(keyword) || 
                            title.includes(keyword)
                        ) || btn.querySelector('svg, mat-icon, .material-icons')?.textContent?.includes('delete');
                    });
                    
                    if (deleteButton) {
                        console.log('Found delete button, clicking it.');
                        deleteButton.click();
                        // 点击后等待一段时间确保操作完成
                        setTimeout(() => resolve({ success: true }), 800);
                    } else {
                        console.warn('Could not find the "Delete" button in the menu.');
                        console.log('Available menu items:', allButtons.map(btn => btn.textContent?.trim()));
                        
                        // 尝试使用键盘快捷键删除
                        try {
                            // 关闭菜单
                            document.body.click();
                            // 选中当前回合
                            turnElement.click();
                            // 等待一下然后按Delete键
                            setTimeout(() => {
                                const deleteEvent = new KeyboardEvent('keydown', {
                                    key: 'Delete',
                                    code: 'Delete',
                                    keyCode: 46,
                                    which: 46,
                                    bubbles: true
                                });
                                document.dispatchEvent(deleteEvent);
                                setTimeout(() => resolve({ success: true }), 500);
                            }, 200);
                        } catch(e) {
                            console.error('Keyboard shortcut failed:', e);
                            // 检查是否为最后一个回合
                            const currentTurnCount = document.querySelectorAll(CHAT_TURN_SELECTOR).length;
                            resolve({ success: false, isLastTurn: currentTurnCount === 1, reason: 'no_delete_button' });
                        }
                    }
                    }, 300); // 等待300ms让菜单内容完全加载
                }
            });

            // 开始观察 DOM 变化以检测菜单出现
            menuObserver.observe(document.body, { childList: true, subtree: true });
            // 点击选项按钮打开菜单
            optionsButton.click();

            // 设置超时处理,防止无限等待
            setTimeout(() => {
                menuObserver.disconnect();
                console.warn('Menu detection timeout');
                resolve({ success: false, error: 'timeout' }); 
            }, 3000);
        });
    }

    /**
     * 为聊天回合添加"从此处删除"按钮
     * @param {Element} chatTurn - 聊天回合元素
     */
    function addDeleteFromHereButton(chatTurn) {
        const optionsButton = chatTurn.querySelector(OPTIONS_BUTTON_SELECTOR);
        // 如果没有选项按钮或已经添加过删除按钮,则跳过
        if (!optionsButton || chatTurn.querySelector('.delete-from-here-btn')) return;

        const btnContainer = optionsButton.parentElement;
        if (!btnContainer) return;

        // 创建删除按钮
        const deleteBtn = document.createElement('button');
        deleteBtn.className = 'delete-from-here-btn';
        deleteBtn.textContent = '❌'; // 垃圾桶图标
        deleteBtn.title = 'Delete From Here (Delete this and all subsequent turns)';
        // 设置按钮样式(完全匹配其他按钮)
        deleteBtn.className = optionsButton.className.replace('mat-mdc-menu-trigger', 'delete-from-here-btn');
        
        // 复制所有计算样式
        const optionsStyle = getComputedStyle(optionsButton);
        Object.assign(deleteBtn.style, {
            background: 'none',
            border: 'none', 
            cursor: 'pointer',
            opacity: '0.6',
            fontSize: '14px',
            transition: 'opacity 0.2s',
            marginRight: '4px',
            // 完全匹配选项按钮的样式
            padding: optionsStyle.padding,
            height: optionsStyle.height,
            width: optionsStyle.width,
            minWidth: optionsStyle.minWidth,
            minHeight: optionsStyle.minHeight,
            display: optionsStyle.display,
            alignItems: optionsStyle.alignItems,
            justifyContent: optionsStyle.justifyContent,
            lineHeight: optionsStyle.lineHeight,
            boxSizing: optionsStyle.boxSizing,
            verticalAlign: optionsStyle.verticalAlign,
            flexShrink: optionsStyle.flexShrink,
            flexGrow: optionsStyle.flexGrow,
            alignSelf: optionsStyle.alignSelf
        });

        // 添加鼠标悬停效果
        deleteBtn.addEventListener('mouseenter', () => deleteBtn.style.opacity = '1');
        deleteBtn.addEventListener('mouseleave', () => deleteBtn.style.opacity = '0.6');

        // 添加点击事件处理器
        deleteBtn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            // 确认删除操作
            if (!confirm('确定要删除此回合及之后的所有对话吗?\nAre you sure you want to delete this and all subsequent turns?')) return;

            // 获取所有聊天回合
            const allTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR));
            const currentIndex = allTurns.indexOf(chatTurn);

            if (currentIndex > -1) {
                // 计算需要删除的回合数量
                const turnsToDeleteCount = allTurns.length - currentIndex;
                console.log(`Planning to delete ${turnsToDeleteCount} turns.`);

                // 从最后一个回合开始逐个删除
                for (let i = 0; i < turnsToDeleteCount; i++) {
                    // 重新获取当前的回合列表(因为删除操作会改变 DOM)
                    const currentTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR));
                    const turnToDelete = currentTurns[currentTurns.length - 1]; // 总是删除最后一个

                    if (turnToDelete) {
                        // 高亮显示即将删除的回合
                        turnToDelete.style.backgroundColor = 'rgba(255, 100, 100, 0.2)';
                        // 使用直接删除方法
                        const directSuccess = await directDelete(turnToDelete);
                        const result = directSuccess ? { success: true } : { success: false };
                        
                        // 如果删除失败,检查原因并处理
                        if (!result.success) {
                            console.log(`删除第 ${i + 1} 个回合失败,原因:`, result);
                            
                            // 检查是否是最后一个回合
                            const remainingTurns = Array.from(document.querySelectorAll(CHAT_TURN_SELECTOR));
                            if (remainingTurns.length === 1) {
                                alert(`无法删除最后一个会话:这是 Google AI Studio 自身的限制。\n\n已成功删除 ${i} 个回合。`);
                                turnToDelete.style.backgroundColor = ''; // 清除红色高亮
                                break; // 停止删除过程
                            } else if (result.reason === 'no_delete_button') {
                                // UI变化导致找不到删除按钮
                                alert(`检测到 Google AI Studio 界面变化,无法找到删除按钮。\n请手动删除或等待脚本更新。\n\n已成功删除 ${i} 个回合。`);
                                turnToDelete.style.backgroundColor = ''; // 清除红色高亮
                                break;
                            } else {
                                // 其他原因导致的失败
                                console.warn('删除操作失败,停止批量删除');
                                turnToDelete.style.backgroundColor = ''; // 清除红色高亮
                                break;
                            }
                        } else {
                            console.log(`成功删除第 ${i + 1} 个回合`);
                            // 快速等待DOM更新
                            await new Promise(resolve => setTimeout(resolve, 300));
                        }
                    } else {
                        console.warn("Could not find a turn to delete. Stopping.");
                        break;
                    }
                }
                console.log('Deletion process finished.');
            }
        });

        // 确保父容器有足够宽度横排显示按钮
        Object.assign(btnContainer.style, {
            display: 'flex',
            flexDirection: 'row',
            alignItems: 'center',
            gap: '4px',
            minWidth: 'auto',
            width: 'auto',
            flexShrink: '0'
        });

        // 将删除按钮插入到选项按钮之前
        btnContainer.insertBefore(deleteBtn, optionsButton);
    }

    /**
     * 处理所有聊天回合,为每个回合添加删除按钮
     */
    function processAllTurns() {
        document.querySelectorAll(CHAT_TURN_SELECTOR).forEach(addDeleteFromHereButton);
    }

    // --- 增强的启动逻辑 ---
    let currentObserver = null;
    
    function initializeScript() {
        const chatSession = document.querySelector(CHAT_SESSION_SELECTOR);
        if (chatSession) {
            console.log('AI Studio Delete Script: Chat session found, initializing.');
            
            // 停止之前的观察器
            if (currentObserver) {
                currentObserver.disconnect();
            }
            
            // 处理现有回合
            processAllTurns();

            // 创建新的观察器
            currentObserver = new MutationObserver(() => {
                setTimeout(processAllTurns, 200);
            });

            // 观察聊天会话变化
            currentObserver.observe(chatSession, { childList: true, subtree: true });
            console.log('AI Studio Delete Script: Observer started.');
            return true;
        }
        return false;
    }
    
    // 初始化
    if (!initializeScript()) {
        // 如果初始化失败,定期重试
        const startupInterval = setInterval(() => {
            if (initializeScript()) {
                clearInterval(startupInterval);
            }
        }, 1000);
    }
    
    // 监听页面变化(新会话)
    const pageObserver = new MutationObserver(() => {
        // 检查是否是新会话或页面变化
        if (document.querySelector(CHAT_SESSION_SELECTOR) && !document.querySelector('.delete-from-here-btn')) {
            setTimeout(initializeScript, 500);
        }
    });
    
    pageObserver.observe(document.body, { childList: true, subtree: true });

})();


/**
 * 模块二:自动中文输出功能
 */
(function() {
    'use strict';
    
    let processedTextareas = new WeakSet();
    let isProcessing = false;
    let lastCheck = 0;
    
    function addText() {
        const now = Date.now();
        if (isProcessing || now - lastCheck < 1000) return;
        
        lastCheck = now;
        isProcessing = true;
        
        try {
            let textarea = document.querySelector('textarea[aria-label="System instructions"]');
            
            if (!textarea) {
                const button = document.querySelector('button[aria-label="System instructions"]');
                if (button && !processedTextareas.has(button)) {
                    button.click();
                    processedTextareas.add(button);
                    setTimeout(() => {
                        textarea = document.querySelector('textarea[aria-label="System instructions"]');
                        if (textarea && !textarea.value.includes('中文')) {
                            processTextarea(textarea);
                        }
                        setTimeout(() => {
                            const closeBtn = document.querySelector('button[aria-label="Close system instructions"]');
                            if (closeBtn) closeBtn.click();
                        }, 200);
                        isProcessing = false;
                    }, 100);
                    return;
                }
            } else if (!textarea.value.includes('中文')) {
                processTextarea(textarea);
            }
        } catch (e) {
            console.error('[脚本错误]:', e);
        }
        
        isProcessing = false;
    }
    
    function processTextarea(textarea) {
        if (textarea && !processedTextareas.has(textarea) && !textarea.value.includes('中文')) {
            const text = '你始终用中文输出';
            
            // 模拟真实用户输入
            textarea.focus();
            textarea.click();
            
            // 使用 document.execCommand 或直接设置
            if (document.execCommand) {
                textarea.value += (textarea.value ? '\n' : '') + text;
                document.execCommand('insertText', false, '');
            } else {
                textarea.value += (textarea.value ? '\n' : '') + text;
            }
            
            // 触发多种事件
            ['focus', 'input', 'change', 'blur', 'keyup'].forEach(eventType => {
                textarea.dispatchEvent(new Event(eventType, { bubbles: true, cancelable: true }));
            });
            
            // 模拟键盘输入
            textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
            textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
            
            processedTextareas.add(textarea);
            console.log('[脚本] 已添加中文输出指令:', textarea.value);
        }
    }
    
    const observer = new MutationObserver(() => {
        if (!isProcessing) addText();
    });
    observer.observe(document.body, { childList: true, subtree: true });
    
    addText();
    setInterval(() => {
        if (!isProcessing) addText();
    }, 3000);
})();


/**
 * 模块三:HTML 预览功能
 */
(function () {
    'use strict';
    // 定义一个全局唯一的策略对象
    if (window.myHtmlPreviewPolicy === undefined) {
        window.myHtmlPreviewPolicy = null;
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            try {
                window.myHtmlPreviewPolicy = trustedTypes.createPolicy('html-preview-policy#userscript', {
                    createHTML: input => input,
                });
            } catch (e) {
                // 如果策略已存在 (例如脚本被注入两次),这会报错,但我们可以忽略
                console.warn("Trusted Types 策略 'html-preview-policy#userscript' 可能已存在。");
            }
        }
    }

    /**
     * 【关键修复】使用 document.createElement 安全地创建模态窗口
     */
    function createModal() {
        if (document.getElementById('html-preview-modal')) return;

        const modalContainer = document.createElement('div');
        modalContainer.id = 'html-preview-modal';

        const overlay = document.createElement('div');
        overlay.className = 'modal-overlay';
        overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 10000; display: none; align-items: center; justify-content: center;`;

        const content = document.createElement('div');
        content.className = 'modal-content';
        content.style.cssText = `background: white; border-radius: 8px; width: 90%; height: 90%; max-width: 1200px; display: flex; flex-direction: column; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);`;

        const header = document.createElement('div');
        header.className = 'modal-header';
        header.style.cssText = `padding: 16px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; color: #333;`;

        const title = document.createElement('h3');
        title.textContent = 'HTML 预览';
        title.style.cssText = 'margin: 0; font-family: sans-serif;';

        const closeBtn = document.createElement('button');
        closeBtn.className = 'close-btn';
        closeBtn.textContent = '×';
        closeBtn.title = '关闭 (Esc)';
        closeBtn.style.cssText = `background: none; border: none; font-size: 28px; cursor: pointer; color: #666; padding: 0 8px; line-height: 1;`;

        const iframe = document.createElement('iframe');
        iframe.className = 'preview-frame';
        iframe.style.cssText = 'width: 100%; flex-grow: 1; border: none;';

        header.appendChild(title);
        header.appendChild(closeBtn);
        content.appendChild(header);
        content.appendChild(iframe);
        overlay.appendChild(content);
        modalContainer.appendChild(overlay);
        document.body.appendChild(modalContainer);

        function closeModal() {
            overlay.style.display = 'none';
            iframe.srcdoc = '';
        }
        closeBtn.addEventListener('click', closeModal);
        overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); });
        document.addEventListener('keydown', e => { if (e.key === 'Escape' && overlay.style.display === 'flex') closeModal(); });
    }

    function showPreview(htmlContent) {
        const modal = document.getElementById('html-preview-modal');
        if (!modal) return;
        const overlay = modal.querySelector('.modal-overlay');
        const iframe = modal.querySelector('.preview-frame');
        const policy = window.myHtmlPreviewPolicy;
        if (policy) {
            iframe.srcdoc = policy.createHTML(htmlContent);
        } else {
            iframe.srcdoc = htmlContent;
        }
        overlay.style.display = 'flex';
    }

    function isHtmlCodeBlock(codeBlock) {
        const titleElement = codeBlock.querySelector('mat-panel-title');
        return titleElement && (titleElement.textContent.toLowerCase().includes('html') || titleElement.textContent.toLowerCase().includes('htm'));
    }

    function addPreviewButton(codeBlock) {
        if (!isHtmlCodeBlock(codeBlock) || codeBlock.querySelector('.html-preview-btn')) return;
        const actionsContainer = codeBlock.querySelector('.actions-container');
        if (!actionsContainer) return;
        const previewBtn = document.createElement('button');
        previewBtn.className = 'html-preview-btn';
        previewBtn.title = '预览 HTML';
        previewBtn.style.cssText = `background: none; border: none; cursor: pointer; padding: 8px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--mat-icon-button-icon-color, currentColor); transition: background-color 0.2s;`;
        const iconSpan = document.createElement('span');
        iconSpan.className = 'material-symbols-outlined notranslate';
        iconSpan.style.fontSize = '20px';
        iconSpan.textContent = 'visibility';
        previewBtn.appendChild(iconSpan);
        previewBtn.addEventListener('mouseenter', () => { previewBtn.style.backgroundColor = 'rgba(0,0,0,0.08)'; });
        previewBtn.addEventListener('mouseleave', () => { previewBtn.style.backgroundColor = 'transparent'; });
        previewBtn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            const codeElement = codeBlock.querySelector('code');
            if (codeElement) showPreview(codeElement.textContent || '');
        });
        actionsContainer.appendChild(previewBtn);
    }

    function processCodeBlocks() {
        document.querySelectorAll('ms-code-block').forEach(addPreviewButton);
    }

    function initializeHtmlPreview() {
        console.log('AI Studio HTML 预览脚本: 初始化...');
        createModal();
        processCodeBlocks();
        const observer = new MutationObserver(processCodeBlocks);
        observer.observe(document.body, { childList: true, subtree: true });
        console.log('AI Studio HTML 预览脚本: 初始化成功。');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeHtmlPreview);
    } else {
        initializeHtmlPreview();
    }
})();