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 8.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 = {
narrationStyle: '*', // '*' = Italics, '**' = Bold, '' = None
removeThinkTags: true,
removeSystemPrompt: true
};
// --- UNIVERSAL SELECTORS (Improved for Edge/Universal support) ---
const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
// Robust selector: looks for specific class or style attributes broadly
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);
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 = 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 ---
function processText(text) {
// 1. Remove tags if enabled
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 system prompt if enabled
if (currentSettings.removeSystemPrompt) {
text = removeSystemPrompt(text);
}
// 3. Format Narration
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, ''); // Strip existing asterisks
// Regex to find quotes or code blocks
if (cleanLine.includes('"') || cleanLine.includes('`')) {
const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
return fragments.map(frag => {
// If it's a quote or code, leave it alone
if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) {
return frag;
}
// If it's narration and not empty, wrap it
return frag.trim() !== '' ? `${wrapper}${frag.trim()}${wrapper}` : '';
}).filter(Boolean).join(' ');
}
// Entire line is narration
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;
}
// --- 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();
// Wait slightly longer for animations/modal
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);
// React/Vue frameworks usually require input events to recognize change
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';
// Settings HTML
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 <think> 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);
// Pre-select dropdown
document.getElementById('setting-narration').value = currentSettings.narrationStyle;
// Event Listeners
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();
}
// --- UI: MAIN BUTTONS ---
function createUI() {
const container = document.createElement('div');
container.id = 'janitor-editor-container';
document.body.appendChild(container);
// 1. Format Button (Pencil)
const formatBtn = document.createElement('button');
formatBtn.innerHTML = '✏️';
formatBtn.id = 'formatter-btn';
formatBtn.title = 'Format (Click) / Move (Drag)';
container.appendChild(formatBtn);
// 2. Settings Button (Gear)
const settingsBtn = document.createElement('button');
settingsBtn.innerHTML = '⚙️';
settingsBtn.id = 'settings-btn';
settingsBtn.title = 'Configure Formatting';
container.appendChild(settingsBtn);
// Initialize Dragging (Drags the whole container)
makeDraggable(container, formatBtn);
// Listeners
settingsBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent drag start
createSettingsModal();
});
}
// --- DRAG LOGIC ---
function makeDraggable(container, handle) {
let isDragging = false;
let wasDragged = false;
let startX, startY, initialLeft, initialTop;
// Load position
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; // Don't drag if clicking settings
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(); // Prevent text selection
}
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;
// Boundary checks
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(`
/* Container */
#janitor-editor-container {
position: fixed;
z-index: 9999;
display: flex;
align-items: flex-end;
gap: 5px;
/* Default Position (Mobile/PC adaptive via media queries below) */
}
/* Buttons */
#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;
}
/* Drag Visuals */
#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); }
/* Modal Styles */
#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; }
/* Screen Sizes Defaults */
@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%; }
}
`);
// --- INIT ---
createUI();
initKeyboardFix();
console.log('Janitor Formatter v8.0 Loaded');
})();