Draggable button with Settings! Select Italics/Bold/Plain text. Edge compatible. Remembers position. Formats narration & dialogues.
// ==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 <think> 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');
})();