Adds a save and download button with a format dropdown to character.AI.
当前为
// ==UserScript==
// @name c.AI Save and Download
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Adds a save and download button with a format dropdown to character.AI.
// @author InariOkami
// @match https://character.ai/*
// @grant none
// @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai
// ==/UserScript==
(async function() {
'use strict';
function createSaveButton() {
const saveChatButton = document.createElement('button');
saveChatButton.innerHTML = 'Chat Options ▼';
saveChatButton.style.position = 'fixed';
saveChatButton.style.top = localStorage.getItem('buttonTop') || '10px';
saveChatButton.style.left = localStorage.getItem('buttonLeft') || '10px';
saveChatButton.style.backgroundColor = '#ff0000';
saveChatButton.style.color = '#ffffff';
saveChatButton.style.padding = '10px';
saveChatButton.style.borderRadius = '5px';
saveChatButton.style.cursor = 'pointer';
saveChatButton.style.zIndex = '1000';
saveChatButton.style.border = 'none';
saveChatButton.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
document.body.appendChild(saveChatButton);
const dropdown = document.createElement('div');
dropdown.style.display = 'none';
dropdown.style.position = 'absolute';
dropdown.style.top = '100%';
dropdown.style.left = '0';
dropdown.style.backgroundColor = '#333';
dropdown.style.border = '1px solid #ccc';
dropdown.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
dropdown.style.zIndex = '1001';
dropdown.style.color = '#ffffff';
dropdown.style.fontFamily = 'sans-serif';
dropdown.style.fontSize = '14px';
dropdown.style.padding = '5px';
saveChatButton.appendChild(dropdown);
const saveButton = document.createElement('button');
saveButton.innerHTML = 'Save Chat';
saveButton.style.display = 'block';
saveButton.style.width = '100%';
saveButton.style.border = 'none';
saveButton.style.padding = '10px';
saveButton.style.cursor = 'pointer';
saveButton.style.backgroundColor = '#444';
saveButton.style.color = '#ffffff'; // White text
saveButton.onclick = saveChat;
dropdown.appendChild(saveButton);
const downloadButton = document.createElement('button');
downloadButton.innerHTML = 'Download Chat';
downloadButton.style.display = 'block';
downloadButton.style.width = '100%';
downloadButton.style.border = 'none';
downloadButton.style.padding = '10px';
downloadButton.style.cursor = 'pointer';
downloadButton.style.backgroundColor = '#444';
downloadButton.style.color = '#ffffff'; // White text
downloadButton.onclick = async function() {
let format = prompt('Enter format (definition/names):', 'definition');
if (format === 'definition' || format === 'names') {
await saveAndDownloadChat(format);
} else {
alert('Invalid format. Please enter "definition" or "names".');
}
};
dropdown.appendChild(downloadButton);
return { saveChatButton, dropdown };
}
function toggleDropdown(dropdown) {
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
}
function makeDraggable(saveChatButton) {
saveChatButton.onmousedown = function(event) {
event.preventDefault();
let shiftX = event.clientX - saveChatButton.getBoundingClientRect().left;
let shiftY = event.clientY - saveChatButton.getBoundingClientRect().top;
document.onmousemove = function(e) {
saveChatButton.style.left = (e.clientX - shiftX) + 'px';
saveChatButton.style.top = (e.clientY - shiftY) + 'px';
};
document.onmouseup = function() {
localStorage.setItem('buttonTop', saveChatButton.style.top);
localStorage.setItem('buttonLeft', saveChatButton.style.left);
document.onmousemove = null;
document.onmouseup = null;
};
};
}
function updateStyles(saveChatButton) {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
saveChatButton.style.backgroundColor = isDarkMode ? '#333' : '#ff0000';
saveChatButton.style.color = isDarkMode ? '#fff' : '#ffffff';
}
var cai_version = -1;
if(location.hostname === "old.character.ai")
cai_version = 1;
else if(location.pathname.startsWith("/chat/"))
cai_version = 2;
else
return alert("Unsupported character.ai version");
var token;
if(cai_version === 1)
token = JSON.parse(localStorage['char_token']).value;
else if(cai_version === 2)
token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;
async function _fetchchats(charid) {
let url = 'https://neo.character.ai/chats/recent/' + charid;
let response = await fetch(url, { headers: { "Authorization": `Token ${token}` } });
let json = await response.json();
return json['chats'];
}
async function getChats(charid) {
let json = await _fetchchats(charid);
return json.map(chat => chat.chat_id);
}
async function getMessages(chat, format) {
let url = 'https://neo.character.ai/turns/' + chat + '/';
let next_token = null;
let turns = [];
do {
let url2 = url;
if (next_token) url2 += "?next_token=" + encodeURIComponent(next_token);
let response = await fetch(url2, { headers: { "Authorization": `Token ${token}` } });
let json = await response.json();
json['turns'].forEach(turn => {
let o = {};
o.author = format === "definition" ? (turn.author.is_human ? "{{user}}" : "{{char}}") : turn.author.name;
o.message = turn.candidates.find(x => x.candidate_id === turn.primary_candidate_id).raw_content || "";
turns.push(o);
});
next_token = json['meta']['next_token'];
} while(next_token);
return turns.reverse();
}
async function getCharacterName(charid) {
let json = await _fetchchats(charid);
return json[0].character_name;
}
async function saveChat() {
const chatElements = document.querySelectorAll('.prose.dark\\:prose-invert');
let chatContent = '';
chatElements.forEach(element => {
chatContent += element.innerText + '\n';
});
localStorage.setItem('savedChat', chatContent);
alert('Chat saved!');
}
async function saveAndDownloadChat(format) {
let char = location.pathname.split("/")[2];
let history = params('hist');
if(history === null) {
let chats = await getChats(char);
history = chats[0];
}
let msgs = await getMessages(history, format);
let str = "";
for(let msg of msgs) {
str += `${msg.author}: ${msg.message}\n`;
}
let date = new Date();
let date_str = `${date.getDate()}-${date.getMonth()+1}-${date.getFullYear()} ${date.getHours()}.${date.getMinutes()}`;
download(`${await getCharacterName(char)} ${date_str}.txt`, str.trimEnd());
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function params(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
function init() {
const { saveChatButton, dropdown } = createSaveButton();
makeDraggable(saveChatButton);
updateStyles(saveChatButton);
window.matchMedia('(prefers-color-scheme: dark)').addListener(() => updateStyles(saveChatButton));
saveChatButton.addEventListener('click', () => toggleDropdown(dropdown));
}
init();
})();