// ==UserScript==
// @name Janitor AI - Automatic Message Formatting Corrector (Drag & Drop button)
// @namespace http://tampermonkey.net/
// @version 7.0
// @description Draggable button with visual feedback! Remembers its position, adapts to screen size, and can't be dragged off-screen. Formats narration/dialogues.
// @author accforfaciet (upgraded by professional developer)
// @match *://janitorai.com/chats/*
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- SCRIPT SETTINGS ---
const DEBUG_MODE = false; // Set to true for console logs
const BUTTON_POSITION_KEY = 'formatterButtonPosition'; // Key for saving position
// --- UNIVERSAL SELECTORS ---
const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
const TEXT_AREA_SELECTOR = 'textarea[style*="font-size: 16px"][style*="!important"]';
const CONFIRM_BUTTON_SELECTOR = 'button[aria-label="Confirm"], button[aria-label="Save"]';
// --- DEBUGGING TOOLS ---
function debugLog(...args) { if (DEBUG_MODE) console.log('[DEBUG]', ...args); }
function waitForElement(selector) {
return new Promise(resolve => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) { observer.disconnect(); resolve(el); }
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// --- CORE TEXT PROCESSING FUNCTIONS ---
function removeThinkTags(text) {
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>', '');
return removeSystemPrompt(text);
}
function formatNarrationAndDialogue(text) {
text = removeThinkTags(text);
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('`')) {
return cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/)
.map(frag => {
if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) return frag;
return frag.trim() !== '' ? `*${frag.trim()}*` : '';
}).filter(Boolean).join(' ');
}
return `*${cleanLine}*`;
}).join('\n');
}
function removeSystemPrompt(text) {
if (!text.trim().toLowerCase().includes('theuser')) return text;
const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
if (splitPointIndex !== -1) {
debugLog(`System prompt found. The text will be trimmed.`);
return text.substring(splitPointIndex + 1);
}
return text;
}
// --- MAIN ACTION SEQUENCE ---
async function processLastMessage(textProcessor) {
debugLog('--- STARTING EDIT PROCESS ---');
try {
const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
if (allEditButtons.length === 0) { debugLog('STOP: No edit buttons found.'); return; }
const lastEditButton = allEditButtons[allEditButtons.length - 1];
lastEditButton.click();
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for modal
const textField = await waitForElement(TEXT_AREA_SELECTOR);
const originalText = textField.value;
const newText = textProcessor(originalText);
textField.value = newText;
textField.dispatchEvent(new Event('input', { bubbles: true }));
const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
if (confirmButton) confirmButton.click();
debugLog('--- PROCESS COMPLETED SUCCESSFULLY ---');
} catch (error) {
console.error('CRITICAL ERROR during edit process:', error);
}
}
/**
* Makes the button draggable, handles clicks, and applies visual effects.
* @param {HTMLElement} button The button element to make draggable.
*/
function makeButtonDraggable(button) {
let isDragging = false;
let wasDragged = false;
let offsetX, offsetY;
// Load saved position
const savedPosition = localStorage.getItem(BUTTON_POSITION_KEY);
if (savedPosition) {
const { top, left } = JSON.parse(savedPosition);
button.style.top = top;
button.style.left = left;
button.style.right = 'auto';
button.style.bottom = 'auto';
}
function dragStart(e) {
e.preventDefault();
isDragging = true;
wasDragged = false;
button.classList.add('is-dragging'); // NEW: Add visual effect class
const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
offsetX = clientX - button.getBoundingClientRect().left;
offsetY = clientY - button.getBoundingClientRect().top;
document.addEventListener('mousemove', dragMove);
document.addEventListener('touchmove', dragMove, { passive: false });
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchend', dragEnd);
}
function dragMove(e) {
if (!isDragging) return;
e.preventDefault();
wasDragged = true;
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
let newLeft = clientX - offsetX;
let newTop = clientY - offsetY;
// Constrain to viewport to prevent dragging off-screen
const buttonRect = button.getBoundingClientRect();
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - buttonRect.width));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - buttonRect.height));
button.style.right = 'auto';
button.style.bottom = 'auto';
button.style.left = `${newLeft}px`;
button.style.top = `${newTop}px`;
}
function dragEnd() {
if (!isDragging) return;
isDragging = false;
button.classList.remove('is-dragging'); // NEW: Remove visual effect class
document.removeEventListener('mousemove', dragMove);
document.removeEventListener('touchmove', dragMove);
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchend', dragEnd);
if (wasDragged) {
// Save the final position
const pos = { top: button.style.top, left: button.style.left };
localStorage.setItem(BUTTON_POSITION_KEY, JSON.stringify(pos));
} else {
// If not dragged, it's a click.
processLastMessage(formatNarrationAndDialogue);
}
}
button.addEventListener('mousedown', dragStart);
button.addEventListener('touchstart', dragStart, { passive: false });
}
/**
* Creates the main button.
*/
function createTriggerButton() {
const buttonContainer = document.createElement('div');
buttonContainer.id = 'janitor-editor-buttons';
document.body.appendChild(buttonContainer);
const formatButton = document.createElement('button');
formatButton.innerHTML = '✏️';
formatButton.id = 'formatterTrigger';
formatButton.title = 'Format asterisks (Click) or Move Button (Drag)';
buttonContainer.appendChild(formatButton);
makeButtonDraggable(formatButton);
}
// --- MOBILE KEYBOARD FIX ---
async function initKeyboardBugFix() {
try {
const mainInput = await waitForElement('textarea[placeholder^="Type a message"]');
const buttonContainer = document.getElementById('janitor-editor-buttons');
if (!mainInput || !buttonContainer) return;
mainInput.addEventListener('focus', () => { buttonContainer.style.display = 'none'; });
mainInput.addEventListener('blur', () => { setTimeout(() => { buttonContainer.style.display = 'block'; }, 200); });
} catch (e) { /* Expected to fail on PC, no error needed */ }
}
// --- ADAPTIVE STYLES ---
GM_addStyle(`
#janitor-editor-buttons button {
position: fixed;
z-index: 9999;
color: white;
border: none;
border-radius: 50%;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
cursor: pointer;
transition: transform 0.2s, opacity 0.2s, box-shadow 0.2s;
user-select: none; /* Prevents text selection while dragging */
}
#janitor-editor-buttons button:active {
/* Kept for quick click feedback, drag effect overrides it */
transform: scale(0.95);
}
#formatterTrigger {
background-color: #c9226e;
}
/* NEW: Visual effect for when the button is being dragged */
#janitor-editor-buttons button.is-dragging {
transform: scale(1.1); /* Make it slightly bigger */
opacity: 0.8; /* Make it semi-transparent */
box-shadow: 0 8px 16px rgba(0,0,0,0.4); /* Enhance the shadow */
transition: none; /* Disable transition for smooth dragging */
}
/* PC STYLES (default position) */
@media (min-width: 769px) {
#formatterTrigger {
width: 50px; height: 50px; font-size: 24px;
right: 27%; bottom: 12%;
}
}
/* MOBILE STYLES (default position) */
@media (max-width: 768px) {
#formatterTrigger {
width: 40px; height: 40px; font-size: 16px;
right: 28%; bottom: 20%;
}
}
`);
// --- LAUNCH SCRIPT ---
createTriggerButton();
initKeyboardBugFix();
console.log('Script "Janitor AI - Automatic Message Formatting Corrector (Drag & Drop button)" (v7.0) launched successfully.');
})();