Universal backup, auto-import, Retention Index sorting for main & search pages with fixed Anti-Bot (>100 msgs/chat), and exact Tag Excluder.
// ==UserScript==
// @name JanitorAI Ultimate Toolkit
// @namespace http://tampermonkey.net/
// @version 3.3
// @description Universal backup, auto-import, Retention Index sorting for main & search pages with fixed Anti-Bot (>100 msgs/chat), and exact Tag Excluder.
// @author CoolGopnik
// @license MIT
// @match https://janitorai.com/*
// @match https://*.janitorai.com/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const retentionCache = new Map();
// Helper: Parse human-readable numbers ("1.5m" -> 1500000, "43k" -> 43000)
function parseFormattedNumber(str) {
if (!str) return 0;
str = String(str).trim().toLowerCase().replace(/,/g, '');
let multiplier = 1;
if (str.endsWith('m')) {
multiplier = 1000000;
str = str.slice(0, -1);
} else if (str.endsWith('k')) {
multiplier = 1000;
str = str.slice(0, -1);
} else if (str.endsWith('b')) {
multiplier = 1000000000;
str = str.slice(0, -1);
}
const num = parseFloat(str);
return isNaN(num) ? 0 : num * multiplier;
}
// Helper: Safely normalize tag strings
function normalizeTag(tag) {
if (!tag || typeof tag !== 'string') return '';
return tag
.toLowerCase()
.replace(/^#/, '')
.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, '')
.trim();
}
// Check if current page is in blacklisted utility pages
function isBlacklistedPage() {
const path = window.location.pathname;
const blacklistedPaths = [
'/my_chats',
'/my_personas',
'/profile-settings',
'/media-library',
'/edit_character'
];
return blacklistedPaths.some(p => path.startsWith(p));
}
// Check if current page is Lorebooks / Scripts
function isLorebookPage() {
const path = window.location.pathname;
return path.startsWith('/scripts') || path.includes('/lorebooks') || !!document.querySelector('._scriptsFullscreenContainer_1s6vr_1');
}
// --- REAL-TIME TAG EXCLUDER / BLACKLIST ---
function applyTagFilter() {
if (isBlacklistedPage() || isLorebookPage()) return;
const rawExcluded = localStorage.getItem('jn_excluded_tags') || '';
if (!rawExcluded.trim()) {
document.querySelectorAll('div[role="group"].profile-character-card-wrapper').forEach(card => {
if (card.dataset.hiddenByTag === 'true') {
card.style.display = '';
delete card.dataset.hiddenByTag;
}
});
return;
}
const excludedTags = rawExcluded
.split(',')
.map(t => normalizeTag(t))
.filter(t => t.length > 0);
if (excludedTags.length === 0) return;
const cards = document.querySelectorAll('div[role="group"].profile-character-card-wrapper');
cards.forEach(card => {
const tagElements = card.querySelectorAll('.pp-cc-tags-item, [class*="pp-tag-"]');
const cardTags = Array.from(tagElements)
.map(el => normalizeTag(el.textContent))
.filter(t => t.length > 0);
const shouldExclude = excludedTags.some(exTag =>
cardTags.some(cardTag => cardTag === exTag)
);
if (shouldExclude) {
card.style.display = 'none';
card.dataset.hiddenByTag = 'true';
} else if (card.dataset.hiddenByTag === 'true') {
card.style.display = '';
delete card.dataset.hiddenByTag;
}
});
}
// Render badge on card UI
function renderCardBadge(card, score, isBotted = false) {
let badge = card.querySelector('.jn-retention-badge');
if (!badge) {
badge = document.createElement('div');
badge.className = 'jn-retention-badge';
badge.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
color: #ffffff;
font-size: 11px;
font-weight: bold;
padding: 3px 8px;
border-radius: 6px;
z-index: 10;
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
`;
card.style.position = 'relative';
card.appendChild(badge);
}
if (isBotted) {
badge.style.background = 'rgba(229, 62, 62, 0.95)';
badge.textContent = `⚠️ Botted`;
} else {
badge.style.background = 'rgba(107, 70, 193, 0.92)';
badge.textContent = `🔥 ${score.toFixed(2)}`;
}
}
function createStyledButton(id, text, onClick) {
const btn = document.createElement('button');
btn.id = id;
btn.textContent = text;
btn.style.padding = '10px 16px';
btn.style.backgroundColor = '#6b46c1';
btn.style.color = '#ffffff';
btn.style.border = 'none';
btn.style.borderRadius = '8px';
btn.style.cursor = 'pointer';
btn.style.fontWeight = 'bold';
btn.style.fontSize = '13px';
btn.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.4)';
btn.style.transition = 'background-color 0.2s ease';
btn.onmouseover = function() { if (!btn.disabled) btn.style.backgroundColor = '#553c9a'; };
btn.onmouseout = function() { if (!btn.disabled) btn.style.backgroundColor = '#6b46c1'; };
btn.onclick = onClick;
return btn;
}
// Inject "Sort by Retention" tab
function injectFilterBarTab() {
const path = window.location.pathname;
const shouldHideRetention = isBlacklistedPage() || isLorebookPage() || path.startsWith('/characters/') || path.startsWith('/create_character');
const existingTab = document.getElementById('jn-retention-tab');
if (shouldHideRetention) {
if (existingTab) existingTab.remove();
return;
}
if (existingTab) return;
const targetContainer = document.querySelector('div[role="tablist"].jn-filter-bar') ||
document.querySelector('.css-hgbogc') ||
document.querySelector('div[role="tablist"]');
if (!targetContainer) return;
const retentionBtn = document.createElement('button');
retentionBtn.id = 'jn-retention-tab';
retentionBtn.type = 'button';
retentionBtn.role = 'tab';
retentionBtn.className = '_tab_1os0o_34 jn-filter-bar__tab';
retentionBtn.style.cssText = `
background-color: #805ad5;
color: #ffffff;
font-weight: bold;
padding: 6px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: background-color 0.2s ease;
`;
retentionBtn.innerHTML = `<span class="_icon_1os0o_88 jn-filter-bar__icon">🔥</span>Sort by Retention`;
retentionBtn.onclick = runRetentionSort;
targetContainer.appendChild(retentionBtn);
}
function injectUI() {
injectFilterBarTab();
applyTagFilter();
const path = window.location.pathname;
const isSingleCharacter = path.startsWith('/characters/');
const isCreatePage = path.startsWith('/create_character');
const isSearchPage = path.startsWith('/search');
const isLorebook = isLorebookPage();
const isBlacklisted = isBlacklistedPage();
const existingContainer = document.getElementById('ai-backup-container');
if (isBlacklisted) {
if (existingContainer) existingContainer.remove();
return;
}
if (existingContainer && document.body.contains(existingContainer)) return;
const container = document.createElement('div');
container.id = 'ai-backup-container';
container.style.position = 'fixed';
container.style.bottom = '20px';
container.style.right = '20px';
container.style.zIndex = '99999';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '8px';
const showTagFilterBox = !isSingleCharacter && !isCreatePage && !isLorebook;
if (showTagFilterBox) {
const tagFilterBox = document.createElement('div');
tagFilterBox.style.cssText = `
background: #1a202c;
padding: 10px 14px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
gap: 4px;
border: 1px solid #4a5568;
`;
const savedTags = localStorage.getItem('jn_excluded_tags') || '';
tagFilterBox.innerHTML = `
<label style="font-size: 11px; font-weight: bold; color: #cbd5e1; font-family: sans-serif;">
🚫 Exclude Tags (comma-separated):
</label>
<input type="text" id="jn-tag-filter-input" value="${savedTags}" placeholder="e.g. Dead Dove, ntr, Male" style="
padding: 6px 10px;
background: #2d3748;
color: #ffffff;
border: 1px solid #4a5568;
border-radius: 6px;
font-size: 12px;
outline: none;
">
`;
container.appendChild(tagFilterBox);
}
if (isCreatePage) {
const btnImport = createStyledButton('ai-btn-import', '📤 Import Character', openImportModal);
container.appendChild(btnImport);
} else if (isSingleCharacter) {
const btn = createStyledButton('ai-btn-single-char', '📥 Save Character Data', runSingleCharacterBackup);
container.appendChild(btn);
} else if (isLorebook) {
const btnActive = createStyledButton('ai-btn-single-lorebook', '📖 Export Active Lorebook', runSingleLorebookBackup);
const btnBulk = createStyledButton('ai-btn-bulk-lorebook', '⚡ Bulk Export All Lorebooks', runBulkLorebookBackup);
container.appendChild(btnActive);
container.appendChild(btnBulk);
} else if (!isSearchPage) {
const btn = createStyledButton('ai-btn-bulk-bots', '⚡ Bulk Backup All Bots', runBulkBackup);
container.appendChild(btn);
}
document.body.appendChild(container);
const inputEl = document.getElementById('jn-tag-filter-input');
if (inputEl) {
inputEl.oninput = function() {
localStorage.setItem('jn_excluded_tags', inputEl.value);
applyTagFilter();
};
}
}
// Helper: Extract stats safely from Next.js JSON
function extractStatsFromNextData(doc) {
const nextDataEl = doc.getElementById('__NEXT_DATA__');
if (!nextDataEl) return null;
try {
const json = JSON.parse(nextDataEl.textContent);
const props = json?.props?.pageProps;
let char = props?.character || props?.dehydratedState?.queries?.[0]?.state?.data;
if (!char && props?.dehydratedState?.queries) {
for (const q of props.dehydratedState.queries) {
if (q?.state?.data?.num_messages || q?.state?.data?.stats?.num_messages) {
char = q.state.data;
break;
}
}
}
if (char) {
const favs = char.stats?.num_favs ?? char.num_favs ?? char.favs_count ?? 0;
const chats = char.stats?.num_chats ?? char.num_chats ?? char.chat_count ?? 0;
const msgs = char.stats?.num_messages ?? char.num_messages ?? char.message_count ?? 0;
return {
likes: parseFormattedNumber(favs),
chats: parseFormattedNumber(chats),
msgs: parseFormattedNumber(msgs)
};
}
} catch (e) {
console.warn('NextData JSON parse error:', e);
}
return null;
}
// --- ACCURATE RETENTION CALCULATION WITH SAFE FALLBACKS ---
async function fetchBotScore(card, url) {
if (retentionCache.has(url)) {
const cached = retentionCache.get(url);
renderCardBadge(card, cached.score, cached.isBotted);
return { element: card, score: cached.score };
}
try {
const response = await fetch(url);
const htmlText = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
// 1. Guaranteed chats count from preview card
const cardChatsEl = card.querySelector('.pp-cc-chats-count');
const cardChatsCount = parseFormattedNumber(cardChatsEl ? cardChatsEl.textContent : '0');
let likesCount = 0;
let chatsCount = 0;
let messagesCount = 0;
// Strategy A: Next.js JSON Extraction
const nextData = extractStatsFromNextData(doc);
if (nextData && (nextData.chats > 0 || nextData.msgs > 0)) {
likesCount = nextData.likes;
chatsCount = nextData.chats;
messagesCount = nextData.msgs;
} else {
// Strategy B: DOM Extraction
const favContainer = doc.querySelector('div[class*="_container_138g0_"]') || doc.querySelector('button[title="favorite"]')?.parentElement;
if (favContainer) {
likesCount = parseFormattedNumber(favContainer.textContent);
}
const ribbonContainer = doc.querySelector('.character-chat-messages-stat-ribbon-tag-hstack') ||
doc.querySelector('[class*="character-chat-messages-stat-ribbon-tag-hstack"]');
if (ribbonContainer) {
const statParagraphs = ribbonContainer.querySelectorAll('p');
const parsedNums = Array.from(statParagraphs).map(p => parseFormattedNumber(p.textContent)).filter(n => n > 0);
if (parsedNums.length >= 2) {
chatsCount = Math.min(parsedNums[0], parsedNums[1]);
messagesCount = Math.max(parsedNums[0], parsedNums[1]);
} else if (parsedNums.length === 1) {
messagesCount = parsedNums[0];
}
}
}
// Fallback: If chats were not parsed from page, use preview card chats count
if (chatsCount <= 0 && cardChatsCount > 0) {
chatsCount = cardChatsCount;
}
// Absolute mathematical check: Messages MUST be >= Chats
if (messagesCount > 0 && chatsCount > 0 && messagesCount < chatsCount) {
const temp = messagesCount;
messagesCount = chatsCount;
chatsCount = temp;
}
// If we don't have BOTH valid numbers, we CANNOT determine retention safely
if (chatsCount <= 0 || messagesCount <= 0) {
const cacheVal = { score: 0, isBotted: false };
retentionCache.set(url, cacheVal);
renderCardBadge(card, 0, false);
return { element: card, score: 0 };
}
const baseMsgsPerChat = messagesCount / chatsCount;
// Anti-bot threshold (> 100 msgs/chat)
if (baseMsgsPerChat > 100) {
const cacheVal = { score: -1, isBotted: true };
retentionCache.set(url, cacheVal);
renderCardBadge(card, 0, true);
return { element: card, score: -1 };
}
const likeRatio = likesCount / chatsCount;
const retentionIndex = baseMsgsPerChat * (1 + likeRatio);
const cacheVal = { score: retentionIndex, isBotted: false };
retentionCache.set(url, cacheVal);
renderCardBadge(card, retentionIndex, false);
return { element: card, score: retentionIndex };
} catch (err) {
console.error('Error fetching bot details:', err);
return { element: card, score: 0 };
}
}
async function runRetentionSort() {
const retentionBtn = document.getElementById('jn-retention-tab');
const cards = Array.from(document.querySelectorAll('div[role="group"].profile-character-card-wrapper'));
if (!cards || cards.length === 0) {
alert('No character cards found on page.');
return;
}
if (retentionBtn) {
retentionBtn.disabled = true;
retentionBtn.style.opacity = '0.7';
}
const listContainer = cards[0].parentElement;
const BATCH_SIZE = 6;
let completed = 0;
const results = [];
for (let i = 0; i < cards.length; i += BATCH_SIZE) {
const batch = cards.slice(i, i + BATCH_SIZE);
const batchPromises = batch.map(card => {
const linkEl = card.querySelector('a.profile-character-card-stack-link-component');
if (!linkEl || !linkEl.href) return Promise.resolve({ element: card, score: 0 });
return fetchBotScore(card, linkEl.href).then(res => {
completed++;
if (retentionBtn) {
const pct = Math.round((completed / cards.length) * 100);
retentionBtn.innerHTML = `⏳ ${pct}% (${completed}/${cards.length})`;
}
return res;
});
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
await delay(50);
}
results.sort((a, b) => b.score - a.score);
results.forEach(item => {
listContainer.appendChild(item.element);
});
applyTagFilter();
if (retentionBtn) {
retentionBtn.disabled = false;
retentionBtn.style.opacity = '1';
retentionBtn.innerHTML = `✅ Sorted!`;
setTimeout(() => {
retentionBtn.innerHTML = `<span class="_icon_1os0o_88 jn-filter-bar__icon" style="margin-right: 4px;">🔥</span>Sort by Retention`;
}, 2500);
}
}
// Convert Rendered HTML Back to Markdown (*, **)
function parseMarkdownContainer(container) {
if (!container) return '';
function convertNode(node) {
if (node.nodeType === Node.TEXT_NODE) return node.textContent;
if (node.nodeType !== Node.ELEMENT_NODE) return '';
const tag = node.tagName.toLowerCase();
const children = Array.from(node.childNodes).map(convertNode).join('');
if (tag === 'strong' || tag === 'b') return children.trim() ? `**${children.trim()}**` : '';
if (tag === 'em' || tag === 'i') return children.trim() ? `*${children.trim()}*` : '';
if (tag === 'code') return children.trim() ? `\`${children.trim()}\`` : '';
if (tag === 'a') {
const href = node.getAttribute('href');
return (href && children.trim()) ? `[${children.trim()}](${href})` : children;
}
if (tag === 'hr') return '\n---\n';
if (tag === 'p' || tag === 'div') return children.trim() ? `${children}\n\n` : '\n';
if (tag === 'br') return '\n';
if (tag === 'h1') return `# ${children}\n\n`;
if (tag === 'h2') return `## ${children}\n\n`;
if (tag === 'h3') return `### ${children}\n\n`;
if (tag === 'li') return `* ${children.trim()}\n`;
return children;
}
let text = convertNode(container);
return text.replace(/\n{3,}/g, '\n\n').trim();
}
// --- BACKUP & IMPORT FUNCTIONS ---
async function runSingleCharacterBackup() {
const btn = document.getElementById('ai-btn-single-char');
if (btn) btn.disabled = true;
await processAndSaveCharacter(document, window.location.href);
if (btn) {
btn.disabled = false;
btn.textContent = '📥 Save Character Data';
}
}
async function runBulkBackup() {
const cards = document.querySelectorAll('div[role="group"].profile-character-card-wrapper');
if (!cards || cards.length === 0) {
alert('No character cards found on this page.');
return;
}
const btn = document.getElementById('ai-btn-bulk-bots');
if (!confirm(`Found ${cards.length} character cards. Start bulk downloading all data, bios, and avatars into folders?`)) {
return;
}
if (btn) {
btn.disabled = true;
btn.style.backgroundColor = '#4a5568';
}
const characterUrls = [];
cards.forEach(card => {
const linkEl = card.querySelector('a.profile-character-card-stack-link-component');
if (linkEl && linkEl.href) {
characterUrls.push(linkEl.href);
}
});
for (let i = 0; i < characterUrls.length; i++) {
const url = characterUrls[i];
if (btn) btn.textContent = `⏳ Downloading ${i + 1}/${characterUrls.length}...`;
try {
const response = await fetch(url);
const htmlText = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
await processAndSaveCharacter(doc, url);
} catch (err) {
console.error(`Failed to backup character at ${url}:`, err);
}
await delay(1500);
}
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = '#6b46c1';
btn.textContent = '✅ Bulk Backup Complete!';
setTimeout(() => {
btn.textContent = '⚡ Bulk Backup All Bots';
}, 3000);
}
}
async function runSingleLorebookBackup() {
const btn = document.getElementById('ai-btn-single-lorebook');
if (btn) btn.disabled = true;
const titleEl = document.querySelector('h2._title_1x34f_32') || document.querySelector('h2') || document.querySelector('[class*="_title_"]');
const lorebookTitle = titleEl ? titleEl.textContent.trim() : 'Lorebook';
const safeTitle = lorebookTitle.replace(/[^a-z0-9_-]/gi, '_');
let cmContent = document.querySelector('.cm-content');
if (!cmContent) {
const jsonToggleBtn = document.querySelector('button[title="JSON Editor"]');
if (jsonToggleBtn) {
jsonToggleBtn.click();
await delay(400);
cmContent = document.querySelector('.cm-content');
}
}
if (!cmContent) {
alert('Could not find the JSON editor. Please switch to the "JSON Editor" tab on the page.');
if (btn) btn.disabled = false;
return;
}
const lines = Array.from(cmContent.querySelectorAll('.cm-line')).map(line => line.textContent);
const rawJsonText = lines.join('\n').trim();
if (!rawJsonText) {
alert('The active Lorebook editor appears to be empty.');
if (btn) btn.disabled = false;
return;
}
let formattedJson = rawJsonText;
try {
const parsed = JSON.parse(rawJsonText);
formattedJson = JSON.stringify(parsed, null, 2);
} catch (e) {
console.warn('Exporting raw text (JSON syntax warning).');
}
saveTextToFolder(formattedJson, `Lorebooks/${safeTitle}.json`, 'application/json');
if (btn) {
btn.disabled = false;
btn.textContent = '✅ Lorebook Exported!';
setTimeout(() => {
btn.textContent = '📖 Export Active Lorebook';
}, 3000);
}
}
async function runBulkLorebookBackup() {
const lorebookCards = document.querySelectorAll('div[class*="_card_"][class*="_clickable_"]');
if (!lorebookCards || lorebookCards.length === 0) {
alert('No lorebook scripts found in the list on this page.');
return;
}
const btn = document.getElementById('ai-btn-bulk-lorebook');
if (!confirm(`Found ${lorebookCards.length} lorebooks in the list. Export all of them as JSON files?`)) {
return;
}
if (btn) {
btn.disabled = true;
btn.style.backgroundColor = '#4a5568';
}
for (let i = 0; i < lorebookCards.length; i++) {
const card = lorebookCards[i];
const titleEl = card.querySelector('[class*="_cardTitle_"]') || card.querySelector('h3');
const lorebookTitle = titleEl ? titleEl.textContent.trim() : `Lorebook_${i + 1}`;
const safeTitle = lorebookTitle.replace(/[^a-z0-9_-]/gi, '_');
if (btn) btn.textContent = `⏳ Exporting ${i + 1}/${lorebookCards.length}: ${lorebookTitle}...`;
card.click();
await delay(600);
let cmContent = document.querySelector('.cm-content');
if (!cmContent) {
const jsonToggleBtn = document.querySelector('button[title="JSON Editor"]');
if (jsonToggleBtn) {
jsonToggleBtn.click();
await delay(400);
cmContent = document.querySelector('.cm-content');
}
}
if (cmContent) {
const lines = Array.from(cmContent.querySelectorAll('.cm-line')).map(line => line.textContent);
const rawJsonText = lines.join('\n').trim();
if (rawJsonText) {
let formattedJson = rawJsonText;
try {
const parsed = JSON.parse(rawJsonText);
formattedJson = JSON.stringify(parsed, null, 2);
} catch (e) {
console.warn(`Raw export for "${lorebookTitle}".`);
}
saveTextToFolder(formattedJson, `Lorebooks/${safeTitle}.json`, 'application/json');
}
}
await delay(300);
}
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = '#6b46c1';
btn.textContent = '✅ All Lorebooks Exported!';
setTimeout(() => {
btn.textContent = '⚡ Bulk Export All Lorebooks';
}, 3000);
}
}
function openImportModal() {
if (document.getElementById('ai-import-modal')) return;
const modal = document.createElement('div');
modal.id = 'ai-import-modal';
modal.style.position = 'fixed';
modal.style.top = '50%';
modal.style.left = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.style.zIndex = '100000';
modal.style.backgroundColor = '#1a202c';
modal.style.color = '#ffffff';
modal.style.padding = '24px';
modal.style.borderRadius = '12px';
modal.style.boxShadow = '0 10px 25px rgba(0,0,0,0.8)';
modal.style.width = '420px';
modal.style.fontFamily = 'sans-serif';
modal.innerHTML = `
<h3 style="margin-top:0; font-size:18px; color:#a78bfa;">📤 Import Character Data</h3>
<p style="font-size:12px; color:#cbd5e1; margin-bottom:16px;">Select exported files to auto-fill the creation form:</p>
<label style="display:block; font-size:12px; margin-bottom:4px; font-weight:bold;">1. Character Info File (_info.txt):</label>
<input type="file" id="ai-import-info-file" accept=".txt" style="margin-bottom:12px; width:100%; font-size:12px;">
<label style="display:block; font-size:12px; margin-bottom:4px; font-weight:bold;">2. Character Bio File (_bio.txt / .html):</label>
<input type="file" id="ai-import-bio-file" accept=".html,.txt" style="margin-bottom:12px; width:100%; font-size:12px;">
<label style="display:block; font-size:12px; margin-bottom:4px; font-weight:bold;">3. Avatar Image (_avatar.webp / image):</label>
<input type="file" id="ai-import-avatar-file" accept="image/*" style="margin-bottom:20px; width:100%; font-size:12px;">
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button id="ai-import-cancel-btn" style="padding:8px 14px; background:#4a5568; color:#fff; border:none; border-radius:6px; cursor:pointer;">Cancel</button>
<button id="ai-import-submit-btn" style="padding:8px 14px; background:#6b46c1; color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:bold;">Import & Fill Form</button>
</div>
`;
document.body.appendChild(modal);
document.getElementById('ai-import-cancel-btn').onclick = () => modal.remove();
document.getElementById('ai-import-submit-btn').onclick = executeImportFormFill;
}
function setNativeInputValue(element, value) {
if (!element) return;
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value')?.set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else if (valueSetter) {
valueSetter.call(element, value);
} else {
element.value = value;
}
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
element.dispatchEvent(new Event('blur', { bubbles: true }));
}
async function executeImportFormFill() {
const infoFile = document.getElementById('ai-import-info-file').files[0];
const bioFile = document.getElementById('ai-import-bio-file').files[0];
const avatarFile = document.getElementById('ai-import-avatar-file').files[0];
if (!infoFile && !bioFile && !avatarFile) {
alert('Please select at least one file to import.');
return;
}
if (avatarFile) {
const fileInput = document.querySelector('input[type="file"][accept*="image"]');
if (fileInput) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(avatarFile);
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (bioFile) {
const bioText = await bioFile.text();
const bioEditor = document.querySelector('div.tiptap.ProseMirror');
if (bioEditor) {
bioEditor.innerHTML = bioText;
bioEditor.dispatchEvent(new Event('input', { bubbles: true }));
bioEditor.dispatchEvent(new Event('blur', { bubbles: true }));
}
}
if (infoFile) {
const infoText = await infoFile.text();
const nameMatch = infoText.match(/^CHARACTER:\s*(.+)$/m);
if (nameMatch && nameMatch[1]) {
const charName = nameMatch[1].trim();
const nameInput = document.getElementById('name');
setNativeInputValue(nameInput, charName);
}
const parseSection = (headerPatterns) => {
const patternStr = Array.isArray(headerPatterns) ? headerPatterns.join('|') : headerPatterns;
const regex = new RegExp(`---\\s*\\[\\s*(?:${patternStr}).*?\\]\\s*---\\n\\n([\\s\\S]*?)(?=\\n\\n---\\s*\\[|$)`, 'i');
const match = infoText.match(regex);
return match ? match[1].trim() : '';
};
const personality = parseSection(['Personality', 'Persona']);
const scenario = parseSection(['Scenario', 'Setting']);
const firstMessage = parseSection(['Initial Messages', 'Initial Message', 'First Message', 'First Messages']);
const exampleDialogs = parseSection(['Example dialogs', 'Example dialog', 'Example Dialogues', 'Example']);
if (personality) setNativeInputValue(document.getElementById('personality'), personality);
if (scenario) setNativeInputValue(document.getElementById('scenario'), scenario);
if (firstMessage) {
const firstMsgInput = document.getElementById('first_messages.0') || document.querySelector('textarea[name="first_messages.0"]');
setNativeInputValue(firstMsgInput, firstMessage);
}
if (exampleDialogs) setNativeInputValue(document.getElementById('example_dialogs'), exampleDialogs);
}
document.getElementById('ai-import-modal').remove();
alert('✅ Character form filled successfully!');
}
async function processAndSaveCharacter(docElement, pageUrl) {
const accordionItems = docElement.querySelectorAll('div[id^="info-"]');
const avatarEl = docElement.querySelector('img.avatar-image') || docElement.querySelector('img[src*="/bot-avatars/"]');
const titleEl = docElement.querySelector('h1') || docElement.querySelector('.pp-cc-name');
let charName = titleEl ? titleEl.textContent.trim() : '';
if (!charName && avatarEl && avatarEl.alt) {
charName = avatarEl.alt.trim();
}
if (!charName) charName = 'Character';
const safeFileName = charName.replace(/[^a-z0-9_-]/gi, '_');
let fullTextContent = `=========================================\n`;
fullTextContent += `CHARACTER: ${charName}\n`;
fullTextContent += `URL: ${pageUrl}\n`;
if (avatarEl && avatarEl.src) {
fullTextContent += `Avatar URL: ${avatarEl.src}\n`;
}
fullTextContent += `=========================================\n\n`;
if (accordionItems && accordionItems.length > 0) {
accordionItems.forEach((item) => {
const titleTextEl = item.querySelector('[class*="characterInfoAccordionTitleText"]');
const sectionTitle = titleTextEl ? titleTextEl.textContent.trim() : item.id;
const markdownContainer = item.querySelector('[class*="characterInfoMarkdownContainer"]');
if (markdownContainer) {
const sectionText = parseMarkdownContainer(markdownContainer);
fullTextContent += `--- [ ${sectionTitle} ] ---\n\n`;
fullTextContent += `${sectionText}\n\n`;
}
});
}
saveTextToFolder(fullTextContent, `${safeFileName}/${safeFileName}_info.txt`, 'text/plain');
let bioHtml = '';
const creatorBox = docElement.querySelector('.character-card-creator-box') || docElement.querySelector('[class*="character-card-creator-box"]');
if (creatorBox) {
const paragraphParent = creatorBox.querySelector('p')?.parentElement;
if (paragraphParent) {
bioHtml = paragraphParent.innerHTML.trim();
} else {
bioHtml = creatorBox.innerHTML.trim();
}
}
if (bioHtml) {
saveTextToFolder(bioHtml, `${safeFileName}/${safeFileName}_bio.txt`, 'text/plain');
}
if (avatarEl && avatarEl.src) {
const ext = avatarEl.src.split('?')[0].split('.').pop() || 'webp';
GM_download({
url: avatarEl.src,
name: `${safeFileName}/${safeFileName}_avatar.${ext}`
});
}
}
function saveTextToFolder(textContent, relativeFilePath, mimeType = 'text/plain') {
const blob = new Blob([textContent], { type: `${mimeType};charset=utf-8;` });
const blobUrl = URL.createObjectURL(blob);
GM_download({
url: blobUrl,
name: relativeFilePath,
onload: () => URL.revokeObjectURL(blobUrl),
onerror: () => URL.revokeObjectURL(blobUrl)
});
}
// SPA Routing & DOM Mutation Monitor
let lastUrl = location.href;
setInterval(() => {
try {
if (location.href !== lastUrl) {
lastUrl = location.href;
const existingContainer = document.getElementById('ai-backup-container');
if (existingContainer) existingContainer.remove();
const retentionTab = document.getElementById('jn-retention-tab');
if (retentionTab) retentionTab.remove();
}
injectUI();
applyTagFilter();
} catch (e) {
console.error('JanitorAI Tool Error:', e);
}
}, 1000);
})();