Universal Text Replacer for Google AI Studio

AI Studioの回答を指定したルールで置換する(デフォルト設定強化版)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Universal Text Replacer for Google AI Studio
// @namespace    https://rentry.co/o9ckxybp
// @version      2.3
// @description  AI Studioの回答を指定したルールで置換する(デフォルト設定強化版)
// @author       ForeverPWA
// @match        *://aistudio.google.com/*
// @match        *://gemini.google.com/* リクエストあれば
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== 設定キー =====
    const CONFIG_KEY = 'universal_replacer_config';
    const defaultConfig = {
        rules: [
            { from: '〇', to: '' },
            { from: '△', to: '未' },
            { from: '□', to: '幼' },
            { from: '・', to: '' },
            { from: '()', to: '' },
            { from: '**', to: '' }
        ]
    };

    let config = Object.assign({}, defaultConfig, GM_getValue(CONFIG_KEY, {}));

    // ===== ユーティリティ関数 =====
    function getModelResponseText() {
        const turns = document.querySelectorAll('ms-chat-turn');
        const lastTurn = turns[turns.length - 1];
        if (!lastTurn) return null;
        const container = lastTurn.querySelector('[data-turn-role="Model"]');
        return container;
    }

    // テキスト置換関数
    function applyReplacementRules(text) {
        if (!text) return '';
        let processedText = text;
        config.rules.forEach(rule => {
            if (rule.from) {
                // 特殊文字のエスケープ処理
                const escapedFrom = rule.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                const regex = new RegExp(escapedFrom, 'g');
                // 変換先が空の場合は削除、それ以外は置換
                const replacement = rule.to || '';
                processedText = processedText.replace(regex, replacement);
            }
        });
        return processedText;
    }

    // ===== メインロジック =====
    let debounceTimer = null;
    function handleUpdate() {
        if (debounceTimer) return;
        debounceTimer = setTimeout(() => {
            debounceTimer = null;
            try {
                const container = getModelResponseText();
                if (!container) return;

                const walker = document.createTreeWalker(
                    container,
                    NodeFilter.SHOW_TEXT,
                    null,
                    false
                );

                let node;
                while (node = walker.nextNode()) {
                    let newText = node.nodeValue;
                    let hasChange = false;

                    config.rules.forEach(rule => {
                        if (rule.from && newText.includes(rule.from)) {
                            hasChange = true;
                        }
                    });

                    if (hasChange) {
                        const replaced = applyReplacementRules(node.nodeValue);
                        if (node.nodeValue !== replaced) {
                            node.nodeValue = replaced;
                        }
                    }
                }
            } catch (e) {
                console.warn("Replacer Script: error:", e);
            }
        }, 300);
    }

    // ===== 設定画面 UI (拡大版) =====
    function openSettings() {
        const overlay = document.createElement('div');
        overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;`;

        const panel = document.createElement('div');
        panel.style.cssText = `background:#202124;color:#e8eaed;padding:30px;border-radius:12px;width:800px;max-height:85vh;overflow-y:auto;box-shadow: 0 4px 20px rgba(0,0,0,0.5);`;

        const title = document.createElement('h2');
        title.textContent = '置換設定';
        title.style.cssText = 'margin:0 0 25px;font-size:1.8em;color:#f28b82;text-align:center;border-bottom: 2px solid #3c4043;padding-bottom: 10px;';
        panel.appendChild(title);

        const rulesContainer = document.createElement('div');
        rulesContainer.style.marginBottom = '25px';

        function createRuleRow(fromVal, toVal) {
            const row = document.createElement('div');
            row.style.cssText = 'display:flex;gap:15px;margin-bottom:12px;align-items:center;';

            const fromInput = document.createElement('input');
            fromInput.placeholder = '変換元';
            fromInput.value = fromVal;
            fromInput.style.cssText = 'flex:1;padding:12px;background:#3c4043;color:#e8eaed;border:1px solid #5f6368;border-radius:6px;font-size:16px;';

            const arrow = document.createElement('span');
            arrow.textContent = '→';
            arrow.style.cssText = 'font-size:1.2em;color:#9aa0a6;font-weight:bold;';

            const toInput = document.createElement('input');
            toInput.placeholder = '変換先 (空で削除)';
            toInput.value = toVal;
            toInput.style.cssText = 'flex:1;padding:12px;background:#3c4043;color:#e8eaed;border:1px solid #5f6368;border-radius:6px;font-size:16px;';

            const delBtn = document.createElement('button');
            delBtn.textContent = '×';
            delBtn.style.cssText = 'background:transparent;color:#f28b82;border:none;cursor:pointer;font-size:1.5em;padding:0 10px;';
            delBtn.onclick = () => row.remove();

            row.appendChild(fromInput);
            row.appendChild(arrow);
            row.appendChild(toInput);
            row.appendChild(delBtn);
            return row;
        }

        config.rules.forEach(rule => {
            rulesContainer.appendChild(createRuleRow(rule.from, rule.to));
        });
        panel.appendChild(rulesContainer);

        const addBtn = document.createElement('button');
        addBtn.textContent = '+ ルールを追加';
        addBtn.style.cssText = 'width:100%;padding:12px;background:#3c4043;color:#8ab4f8;border:2px dashed #5f6368;border-radius:6px;cursor:pointer;margin-bottom:25px;font-size:16px;';
        addBtn.onclick = () => rulesContainer.appendChild(createRuleRow('', ''));
        panel.appendChild(addBtn);

        const actions = document.createElement('div');
        actions.style.cssText = 'display:flex;justify-content:flex-end;gap:15px;border-top:1px solid #3c4043;padding-top:20px;';

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'キャンセル';
        cancelBtn.style.cssText = 'padding:10px 20px;background:transparent;color:#8ab4f8;border:1px solid #5f6368;border-radius:6px;cursor:pointer;';
        cancelBtn.onclick = () => overlay.remove();

        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存して適用';
        saveBtn.style.cssText = 'padding:10px 25px;background:#8ab4f8;color:#202124;border:none;border-radius:6px;cursor:pointer;font-weight:bold;';
        saveBtn.onclick = () => {
            const newRules = [];
            Array.from(rulesContainer.children).forEach(row => {
                const inputs = row.querySelectorAll('input');
                if (inputs[0].value) {
                    newRules.push({ from: inputs[0].value, to: inputs[1].value });
                }
            });
            config.rules = newRules;
            GM_setValue(CONFIG_KEY, config);
            handleUpdate();
            overlay.remove();
        };

        actions.appendChild(cancelBtn);
        actions.appendChild(saveBtn);
        panel.appendChild(actions);
        overlay.appendChild(panel);
        document.body.appendChild(overlay);
    }

    // ===== 初期化 =====
    GM_registerMenuCommand('置換設定', openSettings);

    const observer = new MutationObserver(handleUpdate);
    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(handleUpdate, 2000);

})();