// ==UserScript==
// @name Chub AI - Automatic Message Formatting Corrector (Drag & Drop button)
// @namespace http://tampermonkey.net/
// @version 7.0
// @description Formats narration, removes <think> tags, and features a draggable button that remembers its position. Adapts for PC & Mobile.
// @author accforfaciet (merged & modified by developer assistant)
// @match *://chub.ai/chats/*
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- SCRIPT SETTINGS ---
const DEBUG_MODE = false;
const BUTTON_POSITION_STORAGE_KEY = 'chubFormatterButtonPosition_v2'; // Key for saving the button's position
// --- END OF SETTINGS ---
// --- SELECTORS FOR CHUB.AI ---
const EDIT_BUTTON_SELECTOR = 'button:has(span[aria-label="edit"])';
const TEXT_AREA_SELECTOR = 'textarea.ant-input-borderless';
const CONFIRM_BUTTON_SELECTOR = 'button:has(span[aria-label="check"])';
const MAIN_INPUT_SELECTOR = 'textarea[placeholder*="Send a message"]';
// --- END OF SELECTORS ---
// --- DEBUGGING TOOLS ---
function debugLog(...args) { if (DEBUG_MODE) console.log('[DEBUG]', ...args); }
// --- DOM HELPER ---
function waitForElement(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) return resolve(document.querySelector(selector));
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// --- CORE FORMATTING FUNCTIONS (Unchanged) ---
function removeThinkTags(text) {
text = text.replace(/\n?\s*<thought>[\s\S]*?<\/thought>\s*\n?/g, '');
text = text.replace(/\n?\s*<thoughts>[\s\S]*?<\/thoughts>\s*\n?/g, '');
text = text.replace(/\n?\s*<think>[\s\S]*?<\/think>\s*\n?/g, '');
text = text.replace(/<system>[\s\S]*?<\/system>/g, '');
text = text.replace(/<response>[\s\S]*?<\/response>/g, '');
return removeSystemPrompt(text);
}
function removeSystemPrompt(text) {
const trimmedText = text.trim();
if (!trimmedText.toLowerCase().startsWith('the user')) return text;
const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
if (splitPointIndex !== -1) {
debugLog(`System prompt found and removed.`);
return text.substring(splitPointIndex + 1);
}
return text;
}
function formatNarrationAndDialogue(text) {
text = removeThinkTags(text);
const normalizedText = text.replace(/[«“”„‟⹂❞❝]/g, '"');
const lines = normalizedText.split('\n');
const processedLines = 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]*?`)/);
const processedFragments = fragments.map(frag => {
if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) {
return frag;
} else if (frag.trim() !== '') {
return `*${frag.trim()}*`;
}
return '';
});
return processedFragments.filter(f => f).join(' ');
} else {
return `*${cleanLine}*`;
}
});
return processedLines.join('\n');
}
// --- MAIN SCRIPT LOGIC (Unchanged) ---
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();
const textField = await waitForElement(TEXT_AREA_SELECTOR);
const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeTextareaValueSetter.call(textField, textProcessor(textField.value));
textField.dispatchEvent(new Event('input', { bubbles: true }));
const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
if (confirmButton) confirmButton.click();
debugLog('--- PROCESS SUCCESSFULLY COMPLETED ---');
} catch (error) {
console.error('CRITICAL ERROR during the editing process:', error);
}
}
// --- DRAGGABLE BUTTON LOGIC (Updated!) ---
function makeDraggable(element) {
let isDragging = false;
let hasDragged = false;
let startX, startY, initialLeft, initialTop;
function dragStart(e) {
isDragging = true;
hasDragged = false;
element.classList.add('is-dragging'); // Add visual effect class
const clientX = e.clientX ?? e.touches[0].clientX;
const clientY = e.clientY ?? e.touches[0].clientY;
startX = clientX;
startY = clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
window.addEventListener('mousemove', dragMove, { passive: false });
window.addEventListener('touchmove', dragMove, { passive: false });
window.addEventListener('mouseup', dragEnd);
window.addEventListener('touchend', dragEnd);
}
function dragMove(e) {
if (!isDragging) return;
e.preventDefault(); // Prevent page scroll on mobile
hasDragged = true;
const clientX = e.clientX ?? e.touches[0].clientX;
const clientY = e.clientY ?? e.touches[0].clientY;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
let newLeft = initialLeft + deltaX;
let newTop = initialTop + deltaY;
// --- Screen Boundary Constraints (New!) ---
const buttonWidth = element.offsetWidth;
const buttonHeight = element.offsetHeight;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
newLeft = Math.max(0, Math.min(newLeft, screenWidth - buttonWidth));
newTop = Math.max(0, Math.min(newTop, screenHeight - buttonHeight));
// --- End of Constraints ---
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
}
function dragEnd() {
if (!isDragging) return;
isDragging = false;
element.classList.remove('is-dragging'); // Remove visual effect class
if (hasDragged) {
const finalRect = element.getBoundingClientRect();
savePosition({ left: finalRect.left, top: finalRect.top });
}
window.removeEventListener('mousemove', dragMove);
window.removeEventListener('touchmove', dragMove);
window.removeEventListener('mouseup', dragEnd);
window.removeEventListener('touchend', dragEnd);
}
element.addEventListener('mousedown', dragStart);
element.addEventListener('touchstart', dragStart, { passive: true });
element.addEventListener('click', () => { if (!hasDragged) processLastMessage(formatNarrationAndDialogue); });
}
function savePosition(pos) {
localStorage.setItem(BUTTON_POSITION_STORAGE_KEY, JSON.stringify(pos));
debugLog('Button position saved:', pos);
}
function loadPosition(element) {
const savedPos = localStorage.getItem(BUTTON_POSITION_STORAGE_KEY);
if (savedPos) {
const pos = JSON.parse(savedPos);
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.left = `${pos.left}px`;
element.style.top = `${pos.top}px`;
debugLog('Button position loaded:', pos);
}
}
// --- UI CREATION & INITIALIZATION ---
function createTriggerButton() {
const formatButton = document.createElement('button');
formatButton.innerHTML = '✏️';
formatButton.id = 'formatterTrigger';
formatButton.title = 'Click to format message. Hold and drag to move.';
document.body.appendChild(formatButton);
loadPosition(formatButton);
makeDraggable(formatButton);
}
async function initKeyboardBugFix() {
try {
const mainInput = await waitForElement(MAIN_INPUT_SELECTOR);
const button = document.getElementById('formatterTrigger');
if (!mainInput || !button) return;
mainInput.addEventListener('focus', () => { button.style.display = 'none'; });
mainInput.addEventListener('blur', () => { setTimeout(() => { button.style.display = 'block'; }, 200); });
} catch (e) {
console.log('Could not find main input field for keyboard fix (this is normal on PC).');
}
}
// --- ADAPTIVE STYLES (Updated with drag effects) ---
GM_addStyle(`
#formatterTrigger {
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; /* Smooth transitions */
background-color: #c9226e;
user-select: none;
}
#formatterTrigger:active {
cursor: grabbing;
}
/* Visual feedback class for when the button is being dragged (New!) */
#formatterTrigger.is-dragging {
transform: scale(1.1);
opacity: 0.8;
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
}
/* Default position for MOBILE (if no position is saved) */
#formatterTrigger {
width: 45px; height: 45px; font-size: 20px; right: 5%; bottom: 9%;
}
/* Default position for PC (if no position is saved) */
@media (min-width: 769px) {
#formatterTrigger {
width: 50px; height: 50px; font-size: 24px; right: 18%; bottom: 8%;
}
}
`);
// --- STARTUP ---
createTriggerButton();
initKeyboardBugFix();
console.log('Script "Chub AI - Automatic Message Formatting Corrector (Drag & Drop button)" (v7.0) started successfully.');
})();