Janitor AI - Automatic Message Formatting Corrector (Settings Menu)

The "One-Click" cleanup script. Floating, Inline Top, or Inline Bottom. Select Italics/Bold/Plain text. Edge compatible.

< 脚本 Janitor AI - Automatic Message Formatting Corrector (Settings Menu) 的反馈

评价:一般 - 脚本能用,但还有一些问题

§
发布于:2025-12-23

Request: Less obstructive buttons.

I found the way the buttons are displayed to be obstructive and kind of clunky. Here is a proof of concept that integrates it more into the chat:

// ==UserScript==
// @name         Janitor AI - Automatic Message Formatting Corrector
// @namespace    http://tampermonkey.net/
// @version      9.5
// @description  Adds a Formatter button to the newest message controls.
// @author       accforfaciet (Edited)
// @match        *://janitorai.com/chats/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONSTANTS & DEFAULTS ---
    const SETTINGS_KEY = 'janitorFormatterSettings';

    // --- TARGET CLASS NAMES ---
    const TARGET_MSG_CONTROLS = '._messageControls_2xqwb_18';
    const TARGET_MENU_WRAPPER = '._menuWrapper_hs488_2';

    const DEFAULT_SETTINGS = {
        narrationStyle: '*', 
        removeThinkTags: true,
        removeSystemPrompt: true
    };

    const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
    const TEXT_AREA_SELECTOR = 'textarea[class*="_autoResizeTextarea"], textarea[placeholder^="Type a message"]';

    // Updated selector to include the specific classes provided by the user
    const CONFIRM_BUTTON_SELECTOR = 'button._save_1v19f_205, button._editPanelButton_1v19f_175, button[aria-label="Confirm"], button[type="submit"]';

    let currentSettings = loadSettings();

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : DEFAULT_SETTINGS;
    }

    function saveSettings(newSettings) {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(newSettings));
        currentSettings = newSettings;
    }

    function waitForElement(selector, timeoutMs = 2000) {
        return new Promise(resolve => {
            let el = document.querySelector(selector);
            if (el) return resolve(el);

            const startTime = Date.now();
            const observer = new MutationObserver(() => {
                el = document.querySelector(selector);
                if (el) {
                    observer.disconnect();
                    resolve(el);
                } else if (Date.now() - startTime > timeoutMs) {
                    observer.disconnect();
                    resolve(null);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

    function processText(text) {
        if (currentSettings.removeThinkTags) {
            text = text.replace(/\n?\s*<(thought|thoughts)>[\s\S]*?<\/(thought|thoughts)>\s*\n?/g, '');
            text = text.replace(/<(system|response)>|<\/response>/g, '');
            text = text.replace(/\n?\s*<think>[\s\S]*?<\/think>\s*\n?/g, '');
            text = text.replace('</think>', '');
        }

        if (currentSettings.removeSystemPrompt) {
            text = removeSystemPrompt(text);
        }

        const wrapper = currentSettings.narrationStyle;
        const normalizedText = text.replace(/[«“”„‟⹂❞❝]/g, '"');
        const lines = normalizedText.split('\n');

        return lines.map(line => {
            const trimmedLine = line.trim();
            if (trimmedLine === '') return '';
            const cleanLine = trimmedLine.replace(/\*/g, '');

            if (cleanLine.includes('"') || cleanLine.includes('`')) {
                const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
                return fragments.map(frag => {
                    if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) {
                        return frag;
                    }
                    return frag.trim() !== '' ? `${wrapper}${frag.trim()}${wrapper}` : '';
                }).filter(Boolean).join(' ');
            }
            return `${wrapper}${cleanLine}${wrapper}`;
        }).join('\n');
    }

    function removeSystemPrompt(text) {
        if (!text.trim().toLowerCase().includes('theuser')) return text;
        const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
        if (splitPointIndex !== -1) {
            return text.substring(splitPointIndex + 1);
        }
        return text;
    }

    async function executeFormat() {
        try {
            let textField = document.querySelector(TEXT_AREA_SELECTOR);

            if (!textField || textField.offsetParent === null) {
                const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
                if (allEditButtons.length > 0) {
                    const lastEditButton = allEditButtons[allEditButtons.length - 1];
                    lastEditButton.click();
                    await new Promise(r => setTimeout(r, 600));
                    textField = await waitForElement(TEXT_AREA_SELECTOR);
                }
            }

            if (!textField) return;

            const newText = processText(textField.value);
            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
            nativeInputValueSetter.call(textField, newText);
            textField.dispatchEvent(new Event('input', { bubbles: true }));

            // Automatically click the confirm/save button after a tiny delay
            setTimeout(() => {
                const confirmBtn = document.querySelector(CONFIRM_BUTTON_SELECTOR);
                if (confirmBtn) {
                    confirmBtn.click();
                } else {
                    console.warn('JanitorFormatter: Save button not found using current selectors.');
                }
            }, 150);

        } catch (error) {
            console.error('JanitorFormatter Error:', error);
        }
    }

    function createSettingsModal() {
        if (document.getElementById('janitor-settings-modal')) return;

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'janitor-settings-modal';
        modalOverlay.innerHTML = `
            <div class="janitor-modal-content">
                <h3>Formatter Settings</h3>
                <div class="setting-group">
                    <label>Narration Style:</label>
                    <select id="setting-narration">
                        <option value="*">Italics (*text*)</option>
                        <option value="**">Bold (**text**)</option>
                        <option value="">Plain Text (none)</option>
                    </select>
                </div>
                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-think" ${currentSettings.removeThinkTags ? 'checked' : ''}>
                    <label for="setting-think">Remove &lt;think&gt; tags</label>
                </div>
                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-prompt" ${currentSettings.removeSystemPrompt ? 'checked' : ''}>
                    <label for="setting-prompt">Remove System Prompts</label>
                </div>
                <div class="modal-buttons">
                    <button id="save-settings">Save & Close</button>
                    <button id="cancel-settings" style="background:#555">Cancel</button>
                </div>
            </div>
        `;

        document.body.appendChild(modalOverlay);
        document.getElementById('setting-narration').value = currentSettings.narrationStyle;

        document.getElementById('save-settings').onclick = () => {
            saveSettings({
                narrationStyle: document.getElementById('setting-narration').value,
                removeThinkTags: document.getElementById('setting-think').checked,
                removeSystemPrompt: document.getElementById('setting-prompt').checked
            });
            modalOverlay.remove();
        };

        document.getElementById('cancel-settings').onclick = () => modalOverlay.remove();
    }

    function injectButtons() {
        // Settings Button
        const menuWrapper = document.querySelector(TARGET_MENU_WRAPPER);
        if (menuWrapper && !menuWrapper.querySelector('#janitor-settings-btn-inline')) {
            const settingsBtn = document.createElement('button');
            settingsBtn.id = 'janitor-settings-btn-inline';
            settingsBtn.innerHTML = '⚙️';
            settingsBtn.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();
                createSettingsModal();
            });
            menuWrapper.prepend(settingsBtn);
        }

        // Formatter Button (ONLY NEWEST)
        const allControls = document.querySelectorAll(TARGET_MSG_CONTROLS);
        if (allControls.length === 0) return;

        // Ensure we only have one button at any time
        const existingButtons = document.querySelectorAll('.janitor-formatter-btn-inline');
        const lastControl = allControls[allControls.length - 1];

        // If the button already exists in the last control, do nothing
        if (lastControl.querySelector('.janitor-formatter-btn-inline')) {
            // But remove it from others if found
            existingButtons.forEach(btn => {
                if (!lastControl.contains(btn)) btn.remove();
            });
            return;
        }

        // Otherwise, clean up all and add to newest
        existingButtons.forEach(btn => btn.remove());

        const btn = document.createElement('button');
        btn.className = 'janitor-formatter-btn-inline';
        btn.innerHTML = '✏️';
        btn.addEventListener('click', (e) => {
            e.preventDefault(); e.stopPropagation();
            executeFormat();
        });
        lastControl.prepend(btn);
    }

    // Initialize with a small delay to let the site load
    setTimeout(() => {
        const observer = new MutationObserver(injectButtons);
        observer.observe(document.body, { childList: true, subtree: true });
        injectButtons();
    }, 1000);

    GM_addStyle(`
        ${TARGET_MSG_CONTROLS} {
            display: flex !important;
            flex-direction: row !important;
            align-items: center !important;
        }
        .janitor-formatter-btn-inline {
            background: transparent; border: 1px solid #c9226e; color: #fff;
            border-radius: 4px; cursor: pointer; width: 30px; height: 30px;
            display: inline-flex; justify-content: center; align-items: center;
            font-size: 16px; margin-right: 8px; flex-shrink: 0;
        }
        #janitor-settings-btn-inline {
            background: transparent; border: none; color: #888;
            cursor: pointer; font-size: 18px; margin-right: 15px;
            display: flex; align-items: center;
        }
        #janitor-settings-modal {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.6); z-index: 10000;
            display: flex; justify-content: center; align-items: center;
        }
        .janitor-modal-content {
            background: #1f1f1f; color: white; padding: 20px; border-radius: 12px;
            width: 90%; max-width: 350px; box-shadow: 0 10px 25px rgba(0,0,0,0.5);
        }
        .setting-group { margin-bottom: 15px; display: flex; flex-direction: column; }
        .setting-group.checkbox { flex-direction: row; align-items: center; gap: 10px; }
        .modal-buttons { display: flex; gap: 10px; margin-top: 20px; }
        .modal-buttons button { flex: 1; padding: 10px; border: none; border-radius: 4px; background: #c9226e; color: white; }
    `);
})();
accforfaciet作者
§
发布于:2025-12-24

Subject: Implemented! Inline Buttons + UI Overhaul (v10.0)

Thank you so much for the proof of concept! It was a great idea.

I have released Version 10.0, which fully integrates your suggestion with some extra polish:

  • NEW: 3 Interface Modes:
    1. Floating (Default): The classic draggable button.
    2. Inline Top: Integrates the button into the message control bar (next to Edit/Delete).
    3. Inline Bottom: Integrates the button into the message footer (next to the "Next Message" arrow).
  • NEW: "Reset Position" Button: Lost your floating button off-screen? Open settings and click "Reset Position" to center it instantly.
  • NEW: Text Toggles:
    • Toggle text on the Settings Button (Top Right) to save space on narrow mobile screens.
    • Toggle text on the Format Button to show just the ✏️ icon or "✏️ Auto-Format".
  • Improvement: Floating button now has Size and Opacity sliders.

Thanks for helping make the script better!

发布留言

登录以发布留言。