Janitor AI - Automatic Message Formatting Corrector (Settings Menu)

Draggable button with Settings! Select Italics/Bold/Plain text. Edge compatible. Remembers position. Formats narration & dialogues.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Janitor AI - Automatic Message Formatting Corrector (Settings Menu)
// @namespace    http://tampermonkey.net/
// @version      9.0
// @description  Draggable button with Settings! Select Italics/Bold/Plain text. Edge compatible. Remembers position. Formats narration & dialogues.
// @author       accforfaciet
// @match        *://janitorai.com/chats/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONSTANTS & DEFAULTS ---
    const DEBUG_MODE = false;
    const POSITION_KEY = 'janitorFormatterPosition';
    const SETTINGS_KEY = 'janitorFormatterSettings';

    const DEFAULT_SETTINGS = {
        styleMode: 'italic', // 'italic', 'bold', 'none', 'bold-italic', 'bold-plain'
        removeThinkTags: true,
        removeSystemPrompt: true,
        removeGeneralTags: true
    };

    // --- UNIVERSAL SELECTORS ---
    const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
    const TEXT_AREA_SELECTOR = 'textarea[class*="_autoResizeTextarea"], textarea[placeholder^="Type a message"], textarea[style*="font-size: 16px"]';
    const CONFIRM_BUTTON_SELECTOR = 'button[aria-label="Confirm"], button[aria-label="Save"], button[aria-label*="Confirm"], button[aria-label*="Save"]';

    // --- STATE MANAGEMENT ---
    let currentSettings = loadSettings();

    // --- HELPER FUNCTIONS ---
    function debugLog(...args) { if (DEBUG_MODE) console.log('[DEBUG]', ...args); }

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        // Migration check: if old settings used 'narrationStyle', convert to 'styleMode'
        if (saved) {
            const parsed = JSON.parse(saved);
            if (parsed.narrationStyle !== undefined) {
                if (parsed.narrationStyle === '*') parsed.styleMode = 'italic';
                else if (parsed.narrationStyle === '**') parsed.styleMode = 'bold';
                else parsed.styleMode = 'none';
                delete parsed.narrationStyle;
            }
            return { ...DEFAULT_SETTINGS, ...parsed };
        }
        return DEFAULT_SETTINGS;
    }

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

    function waitForElement(selector, timeoutMs = 5000) {
        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, attributes: true });
        });
    }

    // --- TEXT PROCESSING ENGINE ---
    function processText(text) {
        // 1. Remove <think> tags AND THEIR CONTENT
        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>', '');
        }

        // 2. Remove General Tags (KEEP CONTENT) - New Feature
        // Matches <tag> or </tag> but ignores <think> logic above as that's already gone
        if (currentSettings.removeGeneralTags) {
            text = text.replace(/<\/?[a-zA-Z0-9:-]+(\s[^>]*)?>/g, '');
        }

        // 3. Remove system prompt if enabled
        if (currentSettings.removeSystemPrompt) {
            text = removeSystemPrompt(text);
        }

        // 4. Determine Formatting Wrappers
        let narrWrap = '';
        let dialWrap = '';

        switch (currentSettings.styleMode) {
            case 'italic': narrWrap = '*'; break;
            case 'bold': narrWrap = '**'; break;
            case 'bold-italic': narrWrap = '*'; dialWrap = '**'; break; // Narr=*...*, Dial=**"..."**
            case 'bold-plain': narrWrap = ''; dialWrap = '**'; break;   // Narr=..., Dial=**"..."**
            case 'none': default: break;
        }

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

        const processedLines = lines.map(line => {
            let trimmedLine = line.trim();
            if (trimmedLine === '') return '';

            // Clean existing asterisks to start fresh
            const cleanLine = trimmedLine.replace(/\*/g, '');

            // 5. Noise Filter: Remove lines that are just quotes or asterisks (New Feature)
            // Example: `*"` or `**` or `"`
            if (/^["*]+$/.test(cleanLine)) return null; // Will be filtered out

            // 6. Separator Logic: Handle --- (New Feature)
            // Since we stripped asterisks in `cleanLine`, *---* becomes ---.
            if (cleanLine === '---') return '---';

            // 7. Apply Formatting
            if (cleanLine.includes('"') || cleanLine.includes('`')) {
                const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
                return fragments.map(frag => {
                    const isQuote = (frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'));

                    if (isQuote) {
                        return frag.trim() !== '' ? `${dialWrap}${frag}${dialWrap}` : '';
                    } else {
                        // It is narration
                        return frag.trim() !== '' ? `${narrWrap}${frag.trim()}${narrWrap}` : '';
                    }
                }).filter(Boolean).join(' ');
            }

            // Entire line is narration
            return `${narrWrap}${cleanLine}${narrWrap}`;
        });

        // Filter out null lines (from Noise Filter) and join
        return processedLines.filter(l => l !== null).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;
    }

    // --- MAIN ACTION ---
    async function executeFormat() {
        debugLog('Start Format');
        try {
            const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
            if (allEditButtons.length === 0) return debugLog('No edit buttons');

            const lastEditButton = allEditButtons[allEditButtons.length - 1];
            lastEditButton.click();

            await new Promise(r => setTimeout(r, 600));

            const textField = await waitForElement(TEXT_AREA_SELECTOR);
            if (!textField) return debugLog('Text field not found');

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

            const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
            if (confirmButton) confirmButton.click();

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

    // --- UI: SETTINGS MODAL ---
    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>Formatting Style:</label>
                    <select id="setting-style">
                        <option value="italic">Classic (*Narr* "Dial")</option>
                        <option value="bold">Bold Narration (**Narr** "Dial")</option>
                        <option value="bold-italic">Bold Dial + Italic Narr (*Narr* **"Dial"**)</option>
                        <option value="bold-plain">Bold Dial + Plain Narr (Narr **"Dial"**)</option>
                        <option value="none">Plain Text (No formatting)</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; content</label>
                </div>

                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-gentags" ${currentSettings.removeGeneralTags !== false ? 'checked' : ''}>
                    <label for="setting-gentags">Clean other tags (keep content)</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-style').value = currentSettings.styleMode;

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

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

    // --- UI: MAIN BUTTONS ---
    function createUI() {
        const container = document.createElement('div');
        container.id = 'janitor-editor-container';
        document.body.appendChild(container);

        const formatBtn = document.createElement('button');
        formatBtn.innerHTML = '✏️';
        formatBtn.id = 'formatter-btn';
        formatBtn.title = 'Format (Click) / Move (Drag)';
        container.appendChild(formatBtn);

        const settingsBtn = document.createElement('button');
        settingsBtn.innerHTML = '⚙️';
        settingsBtn.id = 'settings-btn';
        settingsBtn.title = 'Configure Formatting';
        container.appendChild(settingsBtn);

        makeDraggable(container, formatBtn);

        settingsBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            createSettingsModal();
        });
    }

    // --- DRAG LOGIC ---
    function makeDraggable(container, handle) {
        let isDragging = false;
        let wasDragged = false;
        let startX, startY, initialLeft, initialTop;

        const savedPos = localStorage.getItem(POSITION_KEY);
        if (savedPos) {
            const { left, top } = JSON.parse(savedPos);
            container.style.left = left;
            container.style.top = top;
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        }

        function onStart(e) {
            if (e.target.id === 'settings-btn') return;

            isDragging = true;
            wasDragged = false;
            handle.classList.add('is-dragging');

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            startX = clientX;
            startY = clientY;

            const rect = container.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;

            document.addEventListener('mousemove', onMove);
            document.addEventListener('touchmove', onMove, { passive: false });
            document.addEventListener('mouseup', onEnd);
            document.addEventListener('touchend', onEnd);

            e.preventDefault();
        }

        function onMove(e) {
            if (!isDragging) return;
            wasDragged = true;
            e.preventDefault();

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            const dx = clientX - startX;
            const dy = clientY - startY;

            let newLeft = initialLeft + dx;
            let newTop = initialTop + dy;

            const winW = window.innerWidth;
            const winH = window.innerHeight;
            const rect = container.getBoundingClientRect();

            newLeft = Math.max(0, Math.min(newLeft, winW - rect.width));
            newTop = Math.max(0, Math.min(newTop, winH - rect.height));

            container.style.left = `${newLeft}px`;
            container.style.top = `${newTop}px`;
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        }

        function onEnd() {
            isDragging = false;
            handle.classList.remove('is-dragging');
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('touchmove', onMove);
            document.removeEventListener('mouseup', onEnd);
            document.removeEventListener('touchend', onEnd);

            if (wasDragged) {
                localStorage.setItem(POSITION_KEY, JSON.stringify({
                    left: container.style.left,
                    top: container.style.top
                }));
            } else {
                executeFormat();
            }
        }

        handle.addEventListener('mousedown', onStart);
        handle.addEventListener('touchstart', onStart, { passive: false });
    }

    // --- KEYBOARD FIX ---
    async function initKeyboardFix() {
        const input = await waitForElement('textarea[placeholder^="Type a message"]');
        const container = document.getElementById('janitor-editor-container');
        if (input && container) {
            input.addEventListener('focus', () => container.style.display = 'none');
            input.addEventListener('blur', () => setTimeout(() => container.style.display = 'flex', 200));
        }
    }

    // --- STYLES ---
    GM_addStyle(`
        #janitor-editor-container { position: fixed; z-index: 9999; display: flex; align-items: flex-end; gap: 5px; }
        #janitor-editor-container button { border: none; border-radius: 50%; color: white; cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: transform 0.2s, opacity 0.2s; display: flex; justify-content: center; align-items: center; }
        #formatter-btn { background-color: #c9226e; width: 50px; height: 50px; font-size: 24px; }
        #settings-btn { background-color: #444; width: 30px; height: 30px; font-size: 16px; }
        #formatter-btn.is-dragging { transform: scale(1.1); opacity: 0.8; box-shadow: 0 8px 16px rgba(0,0,0,0.5); }
        #janitor-editor-container button:active { transform: scale(0.95); }
        #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); font-family: sans-serif; }
        .janitor-modal-content h3 { margin-top: 0; border-bottom: 1px solid #444; padding-bottom: 10px; }
        .setting-group { margin-bottom: 15px; display: flex; flex-direction: column; }
        .setting-group.checkbox { flex-direction: row; align-items: center; gap: 10px; }
        .setting-group select { padding: 8px; border-radius: 4px; background: #333; color: white; border: 1px solid #555; }
        .modal-buttons { display: flex; gap: 10px; margin-top: 20px; }
        .modal-buttons button { flex: 1; padding: 10px; border: none; border-radius: 4px; cursor: pointer; color: white; background: #c9226e; font-weight: bold; }
        @media (min-width: 769px) { #janitor-editor-container { right: 27%; bottom: 12%; } }
        @media (max-width: 768px) { #formatter-btn { width: 45px; height: 45px; font-size: 20px; } #janitor-editor-container { right: 5%; bottom: 20%; } }
    `);

    createUI();
    initKeyboardFix();
    console.log('Janitor Formatter v9.0 Loaded');
})();