// ==UserScript==
// @name JanitorAI Chat Brancher
// @namespace http://tampermonkey.net/
// @version 0.1.1
// @description Adds buttons to clone chat from any point
// @author IWasTheSyntaxError
// @match https://janitorai.com/chats/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(async function () {
'use strict';
// Utility: get chat ID from URL
function getChatIdFromUrl() {
const m = window.location.pathname.match(/\/chats\/(\d+)/);
if (!m) {
console.error('No chat id in URL.');
return null;
}
return m[1];
}
// Utility: get auth token from cookie
function getAuthTokenFromCookie() {
const match = document.cookie.match(/sb-auth-auth-token=base64-([^;]+)/);
if (!match) return null;
try {
const decoded = JSON.parse(atob(match[1]));
return decoded.access_token;
} catch (e) {
console.error('Failed to decode token:', e);
return null;
}
}
// Utility: fetch JSON with auth token
async function fetchJSON(url, options = {}) {
const token = getAuthTokenFromCookie();
if (!token) throw new Error('No auth token');
const res = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
...(options.headers || {}),
},
});
const text = await res.text().catch(() => '');
let body = text;
try {
body = text ? JSON.parse(text) : text;
} catch (e) {
/* not JSON */
}
if (!res.ok) {
const err = new Error(`HTTP ${res.status}`);
err.status = res.status;
err.body = body;
throw err;
}
return body;
}
// Injected script string for the new tab to post messages
function getInjectedScriptString() {
return `
(function () {
console.log('%c[cloner] Injected script active', 'color: cyan; font-weight:700');
window._cloned_results = [];
window._cloned_done = false;
try {
const banner = document.createElement('div');
banner.id = 'cloner-debug-banner';
banner.style.position = 'fixed';
banner.style.right = '12px';
banner.style.bottom = '12px';
banner.style.zIndex = 2147483647;
banner.style.padding = '8px 10px';
banner.style.background = 'rgba(0,0,0,0.6)';
banner.style.color = 'white';
banner.style.borderRadius = '8px';
banner.style.fontSize = '12px';
banner.style.fontFamily = 'sans-serif';
banner.innerText = 'Cloner: waiting for messages...';
document.documentElement.appendChild(banner);
} catch (e) {}
window.addEventListener('message', function (ev) {
if (!ev.data || ev.data.type !== 'clonedChatMessages') return;
(async function () {
const info = ev.data;
const messages = Array.isArray(info.messages) ? info.messages : [];
const chatId = info.chatId;
const token = info.token;
console.log('[cloner] Received', messages.length, 'messages for chat', chatId);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const postUrl = 'https://janitorai.com/hampter/chats/' + chatId + '/messages';
console.log(messages)
// Send messages in reverse order (last to first)
for (let i = messages.length; i >= 0; --i) {
const msg = messages[i];
const record = { index: i, ok: false, attempts: [] };
const variants = [];
try {
variants.push(JSON.stringify(msg)); // full original
} catch (e) { /* skip if circular */ }
// remove duplicates
const seen = new Set();
const uniqVariants = variants.filter(v => {
if (!v || seen.has(v)) return false;
seen.add(v);
return true;
});
let success = false;
for (let pv = 0; pv < uniqVariants.length && !success; ++pv) {
const payloadText = uniqVariants[pv];
for (let attempt = 1; attempt <= 3 && !success; ++attempt) {
try {
const res = await fetch(postUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: payloadText
});
const text = await res.text().catch(() => '');
let parsed;
try { parsed = text ? JSON.parse(text) : text; } catch (e) { parsed = text; }
record.attempts.push({ variantIndex: pv, attempt, status: res.status, response: parsed, payload: payloadText });
if (res.ok) {
record.ok = true;
record.status = res.status;
record.response = parsed;
success = true;
console.log('[cloner] message', i, 'posted OK (variant', pv, 'attempt', attempt, ')', msg);
break;
} else {
console.warn('[cloner] message', i, 'POST returned', res.status, parsed, '(variant', pv, 'attempt', attempt, ')');
}
} catch (err) {
record.attempts.push({ variantIndex: pv, attempt, error: (err && err.message) ? err.message : String(err), payload: payloadText });
console.error('[cloner] fetch error for message', i, 'variant', pv, 'attempt', attempt, err);
}
await sleep(50 + attempt * 50);
}
}
if (!record.ok) {
console.error('[cloner] FAILED to post message', i, 'record:', record);
}
window._cloned_results.push(record);
await sleep(100);
} // for messages
window._cloned_done = true;
console.log('[cloner] done posting. inspect window._cloned_results');
try {
const banner = document.getElementById('cloner-debug-banner');
if (banner) banner.innerText = 'Cloner: finished';
} catch (e) {}
// Reload page to reflect new messages
await new Promise(r => setTimeout(r, 500));
console.log('[cloner] forcing full reload after message post');
window.location.reload();
})();
}, false);
})();
`.trim();
}
// Wait for chat message elements that have data-item-index attribute
function waitForMessages() {
return new Promise((resolve) => {
const interval = setInterval(() => {
// New selector using data-item-index attribute on messages
const messages = document.querySelectorAll('[data-item-index]');
if (messages.length > 0) {
clearInterval(interval);
resolve(messages);
}
}, 500);
});
}
// Add "Clone from here" buttons to messages
function addCloneButtons(messages) {
messages.forEach((msgEl) => {
if (msgEl.querySelector('.clone-from-here-btn')) return; // skip if already added
const btn = document.createElement('button');
btn.textContent = 'Clone from here';
btn.className = 'clone-from-here-btn';
btn.style.marginLeft = '8px';
btn.style.fontSize = '10px';
btn.style.cursor = 'pointer';
// Read index from data attribute (should be integer)
const index = Number(msgEl.getAttribute('data-item-index'));
if (isNaN(index)) return;
btn.addEventListener('click', () => {
console.log(`[Tampermonkey] Clone from message index: ${index}`);
window.postMessage({ type: 'startPartialClone', startIndex: index }, '*');
});
msgEl.appendChild(btn);
});
}
// Main listener to start cloning partial chats
window.addEventListener('message', async (ev) => {
if (!ev.data || ev.data.type !== 'startPartialClone') return;
const startIndex = ev.data.startIndex;
console.log('[cloner] Partial clone requested from index:', startIndex);
try {
const originalChatId = getChatIdFromUrl();
if (!originalChatId) throw new Error('No chat id in URL');
const source = await fetchJSON(`https://janitorai.com/hampter/chats/${originalChatId}`);
const characterId = source.chat?.character_id;
const messages = Array.isArray(source.chatMessages) ? source.chatMessages : [];
if (!characterId) throw new Error('source chat missing character_id');
if (startIndex < 0 || startIndex >= messages.length) {
alert(`Start index (${startIndex}) out of range. Max index: ${messages.length - 1}`);
return;
}
// Slice messages from 0 up to startIndex inclusive, then reverse
const reversedMessagesToClone = messages.slice(messages.length - (startIndex+1));
reversedMessagesToClone.pop();
// const reversedMessagesToClone = messagesToClone.slice().reverse();
console.log(reversedMessagesToClone);
console.log(`[cloner] Cloning ${reversedMessagesToClone.length} messages (indexes 0 to ${startIndex})`);
const personaId = source.chat?.persona_id || null;
const created = await fetchJSON('https://janitorai.com/hampter/chats', {
method: 'POST',
body: JSON.stringify({ character_id: characterId, persona_id: personaId }),
});
const newChatId = created.id;
console.log('[cloner] Created new chat with id', newChatId);
const newTab = window.open(`https://janitorai.com/chats/${newChatId}`, '_blank');
const token = getAuthTokenFromCookie();
if (!token) throw new Error('Missing auth token for posting');
// Wait for new tab document to be ready and inject script + post messages
await new Promise((resolve, reject) => {
const start = Date.now();
const maxWait = 15000;
const iv = setInterval(() => {
try {
if (Date.now() - start > maxWait) {
clearInterval(iv);
return;
}
if (!newTab.document || !newTab.document.documentElement) return;
const s = newTab.document.createElement('script');
s.type = 'text/javascript';
s.textContent = getInjectedScriptString();
newTab.document.documentElement.appendChild(s);
newTab.postMessage({
type: 'clonedChatMessages',
chatId: newChatId,
messages: reversedMessagesToClone,
token: token,
}, '*');
clearInterval(iv);
resolve();
} catch (e) {
// still waiting for document
}
}, 250);
});
console.log('[cloner] Injection done. New tab should receive messages and start POSTing.');
// alert(`Started cloning ${reversedMessagesToClone.length} messages (0 to ${startIndex}) to new chat ID ${newChatId}.`);
} catch (err) {
console.error('[cloner] error during partial clone:', err);
alert('Error: ' + err.message);
}
});
// Wait for messages and add buttons, re-check every 3 seconds (dynamic updates)
async function run() {
while (true) {
try {
const messages = await waitForMessages();
addCloneButtons(messages);
} catch (e) {
console.error('Error adding clone buttons:', e);
}
await new Promise(r => setTimeout(r, 3000));
}
}
run();
})();