c.AI Enhancements

Adds a save and download button with a format dropdown to character.AI, with widescreen support.

目前为 2024-09-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name c.AI Enhancements
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  5. // @description Adds a save and download button with a format dropdown to character.AI, with widescreen support.
  6. // @author InariOkami
  7. // @match https://character.ai/*
  8. // @grant none
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai
  10. // ==/UserScript==
  11.  
  12. (async function() {
  13. 'use strict';
  14.  
  15. function createSaveButton() {
  16. const saveChatButton = document.createElement('button');
  17. saveChatButton.innerHTML = 'Chat Options ▼';
  18. saveChatButton.style.position = 'fixed';
  19. saveChatButton.style.top = localStorage.getItem('buttonTop') || '10px';
  20. saveChatButton.style.left = localStorage.getItem('buttonLeft') || '10px';
  21. saveChatButton.style.backgroundColor = '#ff0000';
  22. saveChatButton.style.color = '#ffffff';
  23. saveChatButton.style.padding = '10px';
  24. saveChatButton.style.borderRadius = '5px';
  25. saveChatButton.style.cursor = 'pointer';
  26. saveChatButton.style.zIndex = '1000';
  27. saveChatButton.style.border = 'none';
  28. saveChatButton.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
  29. document.body.appendChild(saveChatButton);
  30.  
  31. const dropdown = document.createElement('div');
  32. dropdown.style.display = 'none';
  33. dropdown.style.position = 'absolute';
  34. dropdown.style.top = '100%';
  35. dropdown.style.left = '0';
  36. dropdown.style.backgroundColor = '#ffffff';
  37. dropdown.style.border = '1px solid #ccc';
  38. dropdown.style.boxShadow = '0px 2px 5px rgba(0,0,0,0.2)';
  39. dropdown.style.zIndex = '1001';
  40. dropdown.style.color = '#000000';
  41. dropdown.style.fontFamily = 'sans-serif';
  42. dropdown.style.fontSize = '14px';
  43. dropdown.style.padding = '5px';
  44. saveChatButton.appendChild(dropdown);
  45.  
  46. const saveButton = document.createElement('button');
  47. saveButton.innerHTML = 'Save Chat';
  48. saveButton.style.display = 'block';
  49. saveButton.style.width = '100%';
  50. saveButton.style.border = 'none';
  51. saveButton.style.padding = '10px';
  52. saveButton.style.cursor = 'pointer';
  53. saveButton.style.backgroundColor = '#444';
  54. saveButton.style.color = '#ffffff';
  55. saveButton.onclick = saveChat;
  56. dropdown.appendChild(saveButton);
  57.  
  58. const downloadButton = document.createElement('button');
  59. downloadButton.innerHTML = 'Download Chat';
  60. downloadButton.style.display = 'block';
  61. downloadButton.style.width = '100%';
  62. downloadButton.style.border = 'none';
  63. downloadButton.style.padding = '10px';
  64. downloadButton.style.cursor = 'pointer';
  65. downloadButton.style.backgroundColor = '#444';
  66. downloadButton.style.color = '#ffffff';
  67. downloadButton.onclick = async function() {
  68. let format = prompt('Enter format (definition/names):', 'definition');
  69. if (format === 'definition' || format === 'names') {
  70. await saveAndDownloadChat(format);
  71. } else {
  72. alert('Invalid format. Please enter "definition" or "names".');
  73. }
  74. };
  75. dropdown.appendChild(downloadButton);
  76.  
  77. return { saveChatButton, dropdown };
  78. }
  79.  
  80. function toggleDropdown(dropdown) {
  81. dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
  82. }
  83.  
  84. function makeDraggable(saveChatButton) {
  85. saveChatButton.onmousedown = function(event) {
  86. event.preventDefault();
  87. let shiftX = event.clientX - saveChatButton.getBoundingClientRect().left;
  88. let shiftY = event.clientY - saveChatButton.getBoundingClientRect().top;
  89. document.onmousemove = function(e) {
  90. saveChatButton.style.left = (e.clientX - shiftX) + 'px';
  91. saveChatButton.style.top = (e.clientY - shiftY) + 'px';
  92. };
  93. document.onmouseup = function() {
  94. localStorage.setItem('buttonTop', saveChatButton.style.top);
  95. localStorage.setItem('buttonLeft', saveChatButton.style.left);
  96. document.onmousemove = null;
  97. document.onmouseup = null;
  98. };
  99. };
  100. }
  101.  
  102. function updateStyles(saveChatButton, dropdown) {
  103. const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
  104. saveChatButton.style.backgroundColor = isDarkMode ? '#333' : '#ff0000';
  105. saveChatButton.style.color = isDarkMode ? '#fff' : '#ffffff';
  106. dropdown.style.backgroundColor = isDarkMode ? '#333' : '#ffffff';
  107. dropdown.style.color = isDarkMode ? '#ffffff' : '#000000';
  108. }
  109.  
  110. (function() {
  111. function WideScreen() {
  112. if (document.URL.startsWith("https://old.character.ai/chat")) {
  113. if (document.URL.includes("/chat2") || document.URL.includes("/chat")) {
  114. document.body.getElementsByClassName("apppage").item(0).firstElementChild.attributes.style.value =
  115. "height: 100%; display: flex; flex-direction: column; overflow-y: hidden; min-width: 300px; max-width: 7680; margin: 0px auto;";
  116. document.getElementsByClassName("container-fluid chatbottom").item(0).attributes.item(1).value = "max-width: 7680;";
  117. }
  118. if (document.URL.includes("/chat")) {
  119. document.getElementsByClassName("container-fluid chattop").item(0).attributes.item(1).value = "max-width: 7680";
  120. }
  121. }
  122.  
  123. if (document.URL.startsWith("https://character.ai/chat")) {
  124. var Chat = document.getElementsByClassName(
  125. "overflow-x-hidden overflow-y-scroll px-1 flex flex-col-reverse min-w-full hide-scrollbar"
  126. ).item(0).children;
  127.  
  128. for (var i = 0; i < Chat.length; i++) {
  129. Chat.item(i).style = "min-width:100%";
  130. document.getElementsByClassName("flex w-full flex-col max-w-2xl").item(0).style = "min-width:100%";
  131. }
  132. }
  133. }
  134. setTimeout(() => {
  135. setInterval(WideScreen, 100);
  136. }, 1000);
  137. })();
  138.  
  139. var cai_version = -1;
  140. if(location.hostname === "old.character.ai")
  141. cai_version = 1;
  142. else if(location.pathname.startsWith("/chat/"))
  143. cai_version = 2;
  144. else
  145. return alert("Unsupported character.ai version");
  146.  
  147. var token;
  148. if(cai_version === 1)
  149. token = JSON.parse(localStorage['char_token']).value;
  150. else if(cai_version === 2)
  151. token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;
  152.  
  153. async function _fetchchats(charid) {
  154. let url = 'https://neo.character.ai/chats/recent/' + charid;
  155. let response = await fetch(url, { headers: { "Authorization": `Token ${token}` } });
  156. let json = await response.json();
  157. return json['chats'];
  158. }
  159.  
  160. async function getChats(charid) {
  161. let json = await _fetchchats(charid);
  162. return json.map(chat => chat.chat_id);
  163. }
  164.  
  165. async function getMessages(chat, format) {
  166. let url = 'https://neo.character.ai/turns/' + chat + '/';
  167. let next_token = null;
  168. let turns = [];
  169.  
  170. do {
  171. let url2 = url;
  172. if (next_token) url2 += "?next_token=" + encodeURIComponent(next_token);
  173. let response = await fetch(url2, { headers: { "Authorization": `Token ${token}` } });
  174. let json = await response.json();
  175.  
  176. json['turns'].forEach(turn => {
  177. let o = {};
  178. o.author = format === "definition" ? (turn.author.is_human ? "{{user}}" : "{{char}}") : turn.author.name;
  179. o.message = turn.candidates.find(x => x.candidate_id === turn.primary_candidate_id).raw_content || "";
  180. turns.push(o);
  181. });
  182.  
  183. next_token = json['meta']['next_token'];
  184. } while(next_token);
  185.  
  186. return turns.reverse();
  187. }
  188.  
  189. async function getCharacterName(charid) {
  190. let json = await _fetchchats(charid);
  191. return json[0].character_name;
  192. }
  193.  
  194. async function saveChat() {
  195. const chatElements = document.querySelectorAll('.prose.dark\\:prose-invert');
  196. let chatContent = '';
  197. chatElements.forEach(element => {
  198. chatContent += element.innerText + '\n';
  199. });
  200. const blob = new Blob([chatContent], { type: 'text/plain' });
  201. const url = URL.createObjectURL(blob);
  202. const a = document.createElement('a');
  203. a.href = url;
  204. a.download = 'chat.txt';
  205. a.click();
  206. URL.revokeObjectURL(url);
  207. }
  208.  
  209. async function saveAndDownloadChat(format) {
  210. const charid = location.pathname.split("/")[2];
  211. let chats = await getChats(charid);
  212. let turns = [];
  213. for(let i = 0; i < chats.length; i++)
  214. turns = turns.concat(await getMessages(chats[i], format));
  215. let content = turns.map(turn => `${turn.author}: ${turn.message}`).join("\n\n");
  216. let filename = (await getCharacterName(charid)).replace(/ /g, "_") + ".txt";
  217. let blob = new Blob([content], { type: 'text/plain' });
  218. let url = URL.createObjectURL(blob);
  219. let a = document.createElement('a');
  220. a.href = url;
  221. a.download = filename;
  222. a.click();
  223. URL.revokeObjectURL(url);
  224. }
  225.  
  226. function params(parameterName) {
  227. var result = null,
  228. tmp = [];
  229. location.search
  230. .substr(1)
  231. .split("&")
  232. .forEach(function (item) {
  233. tmp = item.split("=");
  234. if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
  235. });
  236. return result;
  237. }
  238.  
  239. function init() {
  240. const { saveChatButton, dropdown } = createSaveButton();
  241. makeDraggable(saveChatButton);
  242. updateStyles(saveChatButton, dropdown);
  243. window.matchMedia('(prefers-color-scheme: dark)').addListener(() => updateStyles(saveChatButton, dropdown));
  244. saveChatButton.addEventListener('click', () => toggleDropdown(dropdown));
  245. }
  246.  
  247. init();
  248. })();