Persona Impersonator

Impersonation. (RYW-Style)

  1. // ==UserScript==
  2. // @name Persona Impersonator
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Impersonation. (RYW-Style)
  6. // @author Grok 3 (xAI)
  7. // @match https://character.ai/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @setting openrouter_key {type: 'text', default: '', description: 'Your OpenRouter API Key'}
  12. // @setting openrouter_model {type: 'text', default: 'openai/gpt-3.5-turbo', description: 'OpenRouter Model (e.g., openai/gpt-3.5-turbo)'}
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // ### Helper Functions for Fetching Conversation History
  19.  
  20. /** Wraps GM_xmlhttpRequest in a Promise for easier async handling */
  21. function GM_xmlhttpRequestPromise(details) {
  22. return new Promise((resolve, reject) => {
  23. details.onload = function(response) {
  24. resolve(response);
  25. };
  26. details.onerror = function() {
  27. reject(new Error('GM_xmlhttpRequest failed'));
  28. };
  29. GM_xmlhttpRequest(details);
  30. });
  31. }
  32.  
  33. /** Retrieves the access token from a meta tag */
  34. function getAccessToken() {
  35. const meta = document.querySelector('meta[cai_token]');
  36. return meta ? meta.getAttribute('cai_token') : null;
  37. }
  38.  
  39. /** Extracts the character ID from the URL */
  40. function getCharId() {
  41. const path = window.location.pathname.split('/');
  42. if (path[1] === 'chat' && path[2]) {
  43. return path[2];
  44. }
  45. return null;
  46. }
  47.  
  48. /** Fetches the current conversation ID for the character */
  49. async function getCurrentConverId() {
  50. const AccessToken = getAccessToken();
  51. const charId = getCharId();
  52. if (!AccessToken || !charId) return null;
  53.  
  54. try {
  55. const res = await GM_xmlhttpRequestPromise({
  56. method: "GET",
  57. url: `https://neo.character.ai/chats/recent/${charId}`,
  58. headers: {
  59. 'Accept': 'application/json',
  60. 'Content-Type': 'application/json',
  61. "authorization": AccessToken
  62. }
  63. });
  64.  
  65. if (res.status === 200) {
  66. const data = JSON.parse(res.responseText);
  67. if (data.chats && data.chats.length > 0) {
  68. return data.chats[0].chat_id;
  69. }
  70. }
  71. return null;
  72. } catch (error) {
  73. console.error('Error fetching conversation ID:', error);
  74. return null;
  75. }
  76. }
  77.  
  78. /** Fetches all messages in the conversation, handling pagination with custom tagging */
  79. async function fetchMessagesChat2({ AccessToken, converExtId, nextToken = null, turns = [] }) {
  80. let url = `https://neo.character.ai/turns/${converExtId}/`;
  81. if (nextToken) url += `?next_token=${nextToken}`;
  82.  
  83. try {
  84. const res = await GM_xmlhttpRequestPromise({
  85. method: "GET",
  86. url: url,
  87. headers: {
  88. 'Accept': 'application/json',
  89. 'Content-Type': 'application/json',
  90. "authorization": AccessToken
  91. }
  92. });
  93.  
  94. if (res.status === 200) {
  95. const data = JSON.parse(res.responseText);
  96. turns = [...turns, ...data.turns];
  97. if (data.meta.next_token == null) {
  98. const simplifiedChat = turns.map(msg => {
  99. const primary = msg.candidates.find(c => c.candidate_id === msg.primary_candidate_id);
  100. const [alternative] = msg.candidates.slice(-1);
  101. const chosen = primary ?? alternative;
  102. const senderName = msg.author.name || (msg.author.is_human ? 'Human' : 'Axel');
  103. const tag = msg.author.is_human ? `user:${senderName}` : 'assistant';
  104. return {
  105. tag: tag,
  106. message: chosen?.raw_content || "[Message broken]"
  107. };
  108. });
  109. simplifiedChat.reverse(); // Oldest first
  110. return simplifiedChat;
  111. } else {
  112. return fetchMessagesChat2({ AccessToken, converExtId, nextToken: data.meta.next_token, turns });
  113. }
  114. } else {
  115. throw new Error(`Fetch failed: ${res.status}`);
  116. }
  117. } catch (error) {
  118. console.error('Error fetching messages:', error);
  119. throw error;
  120. }
  121. }
  122.  
  123. // ### Core Functions
  124.  
  125. /** Waits for DOM to be ready */
  126. function waitForDOM(callback) {
  127. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  128. callback();
  129. } else {
  130. document.addEventListener('DOMContentLoaded', callback);
  131. const observer = new MutationObserver(() => {
  132. if (document.getElementId('__next')) {
  133. observer.disconnect();
  134. callback();
  135. }
  136. });
  137. observer.observe(document.body, { childList: true, subtree: true });
  138. }
  139. }
  140.  
  141. /** Fetches OpenRouter models and pricing */
  142. function fetchOpenRouterModels(apiKey, callback) {
  143. if (!apiKey) {
  144. callback(null, 'Please enter your OpenRouter API key to fetch models.');
  145. return;
  146. }
  147.  
  148. console.log('Fetching OpenRouter models...');
  149. GM_xmlhttpRequest({
  150. method: 'GET',
  151. url: 'https://openrouter.ai/api/v1/models',
  152. headers: {
  153. 'Authorization': `Bearer ${apiKey}`
  154. },
  155. onload: function(response) {
  156. if (response.status === 200) {
  157. const data = JSON.parse(response.responseText);
  158. callback(data.data, null);
  159. } else {
  160. callback(null, `Error: ${response.status} - ${response.responseText}`);
  161. }
  162. },
  163. onerror: function() {
  164. callback(null, 'Failed to connect to OpenRouter API.');
  165. }
  166. });
  167. }
  168.  
  169. /** Fetches Google Gemini models using the Generative Language API */
  170. function fetchGeminiModels(apiKey, callback) {
  171. if (!apiKey) {
  172. callback(null, 'Please enter your Google Gemini API key to fetch models.');
  173. return;
  174. }
  175.  
  176. console.log('Fetching Google Gemini models...');
  177. GM_xmlhttpRequest({
  178. method: 'GET',
  179. url: 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey,
  180. headers: {
  181. 'Content-Type': 'application/json'
  182. },
  183. onload: function(response) {
  184. if (response.status === 200) {
  185. const data = JSON.parse(response.responseText);
  186. const models = data.models.filter(model =>
  187. model.name.startsWith('models/gemini-1.5-') ||
  188. model.name.startsWith('models/gemini-2.0-') ||
  189. model.name.startsWith('models/aqa') ||
  190. model.name.startsWith('models/gemini-2.5-') ||
  191. model.name.startsWith('models/gemini-embedding-') ||
  192. model.name.startsWith('models/gemini-exp-') ||
  193. model.name.startsWith('models/gemini-ultra') ||
  194. model.name.startsWith('models/gemma-')
  195. ).map(model => ({
  196. id: model.name.split('/').pop(),
  197. name: model.displayName || model.name.split('/').pop(),
  198. pricing: { prompt: 'N/A', completion: 'N/A' }
  199. }));
  200. callback(models, null);
  201. } else {
  202. console.error('Failed to fetch Gemini models:', response.status, response.responseText);
  203. // Fallback to hardcoded list if API call fails
  204. const fallbackModels = [
  205. { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
  206. { id: 'gemini-1.0-pro-001', name: 'Gemini 1.0 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  207. { id: 'gemini-1.5-pro-002', name: 'Gemini 1.5 Pro 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
  208. { id: 'gemini-1.5-pro-001', name: 'Gemini 1.5 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  209. { id: 'gemini-1.5-flash-002', name: 'Gemini 1.5 Flash 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
  210. { id: 'gemini-1.5-flash-001', name: 'Gemini 1.5 Flash 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  211. { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', pricing: { prompt: 'N/A', completion: 'N/A' } },
  212. { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
  213. { id: 'gemini-2.0-pro-vision', name: 'Gemini 2.0 Pro Vision', pricing: { prompt: 'N/A', completion: 'N/A' } },
  214. { id: 'gemini-2.0-nano', name: 'Gemini 2.0 Nano', pricing: { prompt: 'N/A', completion: 'N/A' } },
  215. { id: 'gemini-exp-0827', name: 'Gemini Exp 0827', pricing: { prompt: 'N/A', completion: 'N/A' } },
  216. { id: 'gemini-exp-0924', name: 'Gemini Exp 0924', pricing: { prompt: 'N/A', completion: 'N/A' } },
  217. { id: 'gemma-3-27b-it', name: 'Gemma 3 27B IT', pricing: { prompt: 'N/A', completion: 'N/A' } }
  218. ];
  219. callback(fallbackModels, `Error: ${response.status} - ${response.responseText}`);
  220. }
  221. },
  222. onerror: function() {
  223. console.error('Network error fetching Gemini models');
  224. const fallbackModels = [
  225. { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
  226. { id: 'gemini-1.0-pro-001', name: 'Gemini 1.0 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  227. { id: 'gemini-1.5-pro-002', name: 'Gemini 1.5 Pro 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
  228. { id: 'gemini-1.5-pro-001', name: 'Gemini 1.5 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  229. { id: 'gemini-1.5-flash-002', name: 'Gemini 1.5 Flash 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
  230. { id: 'gemini-1.5-flash-001', name: 'Gemini 1.5 Flash 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
  231. { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', pricing: { prompt: 'N/A', completion: 'N/A' } },
  232. { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
  233. { id: 'gemini-2.0-pro-vision', name: 'Gemini 2.0 Pro Vision', pricing: { prompt: 'N/A', completion: 'N/A' } },
  234. { id: 'gemini-2.0-nano', name: 'Gemini 2.0 Nano', pricing: { prompt: 'N/A', completion: 'N/A' } },
  235. { id: 'gemini-exp-0827', name: 'Gemini Exp 0827', pricing: { prompt: 'N/A', completion: 'N/A' } },
  236. { id: 'gemini-exp-0924', name: 'Gemini Exp 0924', pricing: { prompt: 'N/A', completion: 'N/A' } },
  237. { id: 'gemma-3-27b-it', name: 'Gemma 3 27B IT', pricing: { prompt: 'N/A', completion: 'N/A' } }
  238. ];
  239. callback(fallbackModels, 'Failed to connect to Google Gemini API.');
  240. }
  241. });
  242. }
  243.  
  244. /** Generates text using OpenRouter API */
  245. function generateOpenRouterText(apiKey, model, messages, output, copyBtn) {
  246. const payload = {
  247. model: model,
  248. messages: messages.map(msg => ({
  249. role: msg.tag.startsWith('user:') ? 'user' : msg.tag,
  250. content: msg.message
  251. }))
  252. };
  253.  
  254. console.log('Sending request to OpenRouter:', payload);
  255. GM_xmlhttpRequest({
  256. method: 'POST',
  257. url: 'https://openrouter.ai/api/v1/chat/completions',
  258. headers: {
  259. 'Content-Type': 'application/json',
  260. 'Authorization': `Bearer ${apiKey}`
  261. },
  262. data: JSON.stringify(payload),
  263. onload: function(response) {
  264. if (response.status === 200) {
  265. const data = JSON.parse(response.responseText);
  266. const generatedText = data.choices[0].message.content.trim();
  267. output.value = generatedText;
  268. copyBtn.style.display = 'block';
  269. console.log('Generated:', generatedText);
  270. } else {
  271. output.value = `Error: ${response.status} - ${response.responseText}`;
  272. copyBtn.style.display = 'none';
  273. }
  274. },
  275. onerror: function() {
  276. output.value = 'Failed to connect to OpenRouter API.';
  277. copyBtn.style.display = 'none';
  278. }
  279. });
  280. }
  281.  
  282. /** Generates text using Google Gemini API with temperature */
  283. function generateGeminiText(apiKey, model, messages, temperature, output, copyBtn) {
  284. const payload = {
  285. contents: [{
  286. parts: messages.map(msg => ({
  287. text: `${msg.tag}: ${msg.message}`
  288. }))
  289. }],
  290. generationConfig: {
  291. temperature: parseFloat(temperature) || 1.0,
  292. maxOutputTokens: 2048
  293. }
  294. };
  295.  
  296. console.log('Sending request to Google Gemini:', payload);
  297. console.log(model, payload);
  298. GM_xmlhttpRequest({
  299. method: 'POST',
  300. url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
  301. headers: {
  302. 'Content-Type': 'application/json'
  303. },
  304. data: JSON.stringify(payload),
  305. onload: function(response) {
  306. if (response.status === 200) {
  307. const data = JSON.parse(response.responseText);
  308. const generatedText = data.candidates[0].content.parts[0].text.trim();
  309. output.value = generatedText;
  310. copyBtn.style.display = 'block';
  311. console.log('Generated:', generatedText);
  312. } else {
  313. output.value = `Error: ${response.status} - ${response.responseText}`;
  314. copyBtn.style.display = 'none';
  315. }
  316. },
  317. onerror: function() {
  318. output.value = 'Failed to connect to Google Gemini API.';
  319. copyBtn.style.display = 'none';
  320. }
  321. });
  322. }
  323.  
  324. // ### UI Creation
  325.  
  326. function createUI() {
  327. const storedPersona = localStorage.getItem('cai_persona') || '';
  328. const storedApiSelection = localStorage.getItem('cai_api_selection') || 'openrouter';
  329. const storedOpenRouterKey = GM_getValue('openrouter_key', '');
  330. const storedGeminiKey = localStorage.getItem('cai_gemini_key') || '';
  331. const storedModel = GM_getValue('openrouter_model', 'openai/gpt-3.5-turbo');
  332. const storedInput = localStorage.getItem('cai_input') || '';
  333. const storedTemperature = localStorage.getItem('cai_gemini_temperature') || '1.0';
  334.  
  335. // Inject CSS
  336. const style = document.createElement('style');
  337. style.innerHTML = `
  338. @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
  339. .ptrk_main {
  340. position: fixed !important;
  341. display: flex;
  342. flex-direction: column;
  343. margin: 0;
  344. z-index: 10000 !important;
  345. min-width: 300px;
  346. background-color: rgba(33, 37, 41, 0.95);
  347. right: 0;
  348. top: 0;
  349. height: 100vh;
  350. padding: 18px;
  351. color: white;
  352. font-family: "Noto Sans", sans-serif;
  353. font-size: 13px;
  354. transition: transform 0.3s ease;
  355. width: 470px;
  356. box-sizing: border-box;
  357. box-shadow: -2px 0 5px rgba(0, 0, 0, 0.5);
  358. overflow-y: auto;
  359. }
  360. .ptrk_main.ptrk_hidden {
  361. transform: translateX(100%);
  362. }
  363. .ptrk_toggle_btn {
  364. position: fixed !important;
  365. top: 105px;
  366. right: 10px;
  367. z-index: 10001;
  368. background-color: rgba(33, 37, 41, 0.95);
  369. color: white;
  370. padding: 8px 16px;
  371. border-radius: 5px;
  372. cursor: pointer;
  373. font-family: "Noto Sans", sans-serif;
  374. font-size: 14px;
  375. transition: background-color 0.3s;
  376. box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
  377. }
  378. .ptrk_toggle_btn:hover {
  379. background-color: rgba(50, 55, 60, 0.95);
  380. }
  381. .ptrk_main fieldset {
  382. border: 1px solid rgb(59, 59, 63);
  383. border-radius: 3px;
  384. padding: 10px;
  385. margin-bottom: 10px;
  386. }
  387. .ptrk_main legend {
  388. font-size: 12px;
  389. padding: 0 5px;
  390. }
  391. .ptrk_main input, .ptrk_main textarea, .ptrk_main select {
  392. width: 100%;
  393. color: #d1d5db;
  394. padding: 10px;
  395. margin: 5px 0;
  396. box-sizing: border-box;
  397. font-size: 12px;
  398. background: rgba(0, 0, 0, 0.2);
  399. border: 1px solid #8e8e8e;
  400. border-radius: 3px;
  401. -webkit-appearance: none;
  402. -moz-appearance: none;
  403. appearance: none;
  404. }
  405. .ptrk_main select {
  406. cursor: pointer;
  407. background: rgba(0, 0, 0, 0.2) url('data:image/svg+xml;utf8,<svg fill="%23d1d5db" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>') no-repeat right 10px center;
  408. }
  409. .ptrk_main select::-ms-expand {
  410. display: none;
  411. }
  412. .ptrk_main select option {
  413. color: #d1d5db;
  414. background: rgba(33, 37, 41, 0.95);
  415. }
  416. .ptrk_main textarea {
  417. resize: vertical;
  418. }
  419. .ptrk_main textarea[readonly] {
  420. background: rgba(255, 255, 255, 0.05);
  421. border-color: rgba(255, 255, 255, 0.1);
  422. }
  423. .ptrk_main .abtn {
  424. cursor: pointer;
  425. padding: 6px 12px;
  426. border-radius: 3px;
  427. font-weight: bold;
  428. margin: 2px;
  429. background: rgb(95, 99, 101);
  430. text-align: center;
  431. transition: background 0.2s;
  432. }
  433. .ptrk_main .abtn:hover {
  434. background: rgb(118, 123, 125);
  435. }
  436. .ptrk_main .midbtns {
  437. display: flex;
  438. justify-content: center;
  439. margin-top: 5px;
  440. }
  441. .ptrk_models_table {
  442. max-height: 200px;
  443. overflow-y: auto;
  444. margin-top: 5px;
  445. }
  446. .ptrk_models_table table {
  447. width: 100%;
  448. border-collapse: collapse;
  449. font-size: 11px;
  450. color: #d1d5db;
  451. background: rgba(0, 0, 0, 0.5);
  452. }
  453. .ptrk_models_table th, .ptrk_models_table td {
  454. border: 1px solid rgb(45, 45, 48);
  455. padding: 5px;
  456. text-align: left;
  457. }
  458. .ptrk_models_table th {
  459. background: rgba(0, 0, 0, 0.6);
  460. }
  461. `;
  462. document.head.appendChild(style);
  463.  
  464. // Toggle button
  465. const toggleBtn = document.createElement('div');
  466. toggleBtn.classList.add('ptrk_toggle_btn');
  467. toggleBtn.textContent = 'Hide UI';
  468. document.body.appendChild(toggleBtn);
  469.  
  470. // Main UI container
  471. const mainDom = document.createElement('div');
  472. mainDom.classList.add('ptrk_main');
  473. mainDom.innerHTML = `
  474. <fieldset>
  475. <legend>API Selection</legend>
  476. <select id="api-selection">
  477. <option value="openrouter" ${storedApiSelection === 'openrouter' ? 'selected' : ''}>OpenRouter</option>
  478. <option value="gemini" ${storedApiSelection === 'gemini' ? 'selected' : ''}>Google Gemini</option>
  479. </select>
  480. </fieldset>
  481. <fieldset id="openrouter-key-field" style="${storedApiSelection === 'openrouter' ? '' : 'display: none;'}">
  482. <legend>OpenRouter API Key</legend>
  483. <input id="api-key" type="text" placeholder="Enter your OpenRouter API key..." value="${storedOpenRouterKey}">
  484. </fieldset>
  485. <fieldset id="gemini-key-field" style="${storedApiSelection === 'gemini' ? '' : 'display: none;'}">
  486. <legend>Google Gemini API Key</legend>
  487. <input id="gemini-key" type="text" placeholder="Enter your Gemini API key..." value="${storedGeminiKey}">
  488. </fieldset>
  489. <fieldset id="gemini-temp-field" style="${storedApiSelection === 'gemini' ? '' : 'display: none;'}">
  490. <legend>Temperature (0.0 - 2.0)</legend>
  491. <input id="gemini-temperature" type="number" step="0.1" min="0" max="2" placeholder="Enter temperature (default 1.0)" value="${storedTemperature}">
  492. </fieldset>
  493. <fieldset>
  494. <legend>Model</legend>
  495. <select id="model-select">
  496. <option value="${storedModel}">${storedModel} (default)</option>
  497. </select>
  498. </fieldset>
  499. <fieldset>
  500. <legend>Available Models</legend>
  501. <div class="ptrk_models_table">
  502. <table>
  503. <thead>
  504. <tr>
  505. <th>Model Name</th>
  506. <th>Input ($/1k)</th>
  507. <th>Output ($/1k)</th>
  508. </tr>
  509. </thead>
  510. <tbody id="models-table-body">
  511. <tr><td colspan="3">Loading models...</td></tr>
  512. </tbody>
  513. </table>
  514. </div>
  515. </fieldset>
  516. <fieldset>
  517. <legend>Persona</legend>
  518. <textarea id="persona-input" placeholder="Enter your persona here...">${storedPersona}</textarea>
  519. </fieldset>
  520. <fieldset>
  521. <legend>Input</legend>
  522. <textarea id="user-input" placeholder="Enter message to impersonate...">${storedInput}</textarea>
  523. <div class="midbtns">
  524. <div class="abtn" data-tag="generate">Generate Text</div>
  525. <div class="abtn" data-tag="generate-next">Generate Next Response</div>
  526. </div>
  527. </fieldset>
  528. <fieldset>
  529. <legend>Output</legend>
  530. <textarea id="output-text" readonly></textarea>
  531. <div class="midbtns">
  532. <div class="abtn" data-tag="copy" style="display: none;">Copy to Clipboard</div>
  533. </div>
  534. </fieldset>
  535. `;
  536. document.body.appendChild(mainDom);
  537.  
  538. // UI elements
  539. const apiSelection = mainDom.querySelector('#api-selection');
  540. const openRouterKeyField = mainDom.querySelector('#openrouter-key-field');
  541. const geminiKeyField = mainDom.querySelector('#gemini-key-field');
  542. const geminiTempField = mainDom.querySelector('#gemini-temp-field');
  543. const apiKeyInput = mainDom.querySelector('#api-key');
  544. const geminiKeyInput = mainDom.querySelector('#gemini-key');
  545. const geminiTempInput = mainDom.querySelector('#gemini-temperature');
  546. const modelSelect = mainDom.querySelector('#model-select');
  547. const modelsTableBody = mainDom.querySelector('#models-table-body');
  548. const personaInput = mainDom.querySelector('#persona-input');
  549. const input = mainDom.querySelector('#user-input');
  550. const generateBtn = mainDom.querySelector('[data-tag="generate"]');
  551. const generateNextBtn = mainDom.querySelector('[data-tag="generate-next"]');
  552. const output = mainDom.querySelector('#output-text');
  553. const copyBtn = mainDom.querySelector('[data-tag="copy"]');
  554.  
  555. // Toggle UI visibility
  556. let isHidden = false;
  557. toggleBtn.addEventListener('click', () => {
  558. isHidden = !isHidden;
  559. mainDom.classList.toggle('ptrk_hidden', isHidden);
  560. toggleBtn.textContent = isHidden ? 'Show UI' : 'Hide UI';
  561. });
  562.  
  563. // API selection change
  564. apiSelection.addEventListener('change', () => {
  565. const selection = apiSelection.value;
  566. localStorage.setItem('cai_api_selection', selection);
  567. if (selection === 'openrouter') {
  568. openRouterKeyField.style.display = '';
  569. geminiKeyField.style.display = 'none';
  570. geminiTempField.style.display = 'none';
  571. updateModels('openrouter');
  572. } else if (selection === 'gemini') {
  573. openRouterKeyField.style.display = 'none';
  574. geminiKeyField.style.display = '';
  575. geminiTempField.style.display = '';
  576. updateModels('gemini');
  577. }
  578. });
  579.  
  580. // Save OpenRouter API key
  581. apiKeyInput.addEventListener('change', () => {
  582. const apiKey = apiKeyInput.value.trim();
  583. GM_setValue('openrouter_key', apiKey);
  584. if (apiSelection.value === 'openrouter') {
  585. updateModels('openrouter');
  586. }
  587. });
  588.  
  589. // Save Gemini API key
  590. geminiKeyInput.addEventListener('change', () => {
  591. const apiKey = geminiKeyInput.value.trim();
  592. localStorage.setItem('cai_gemini_key', apiKey);
  593. if (apiSelection.value === 'gemini') {
  594. updateModels('gemini');
  595. }
  596. });
  597.  
  598. // Save Gemini temperature
  599. geminiTempInput.addEventListener('change', () => {
  600. const temperature = geminiTempInput.value.trim();
  601. localStorage.setItem('cai_gemini_temperature', temperature);
  602. });
  603.  
  604. // Save selected model
  605. modelSelect.addEventListener('change', () => {
  606. const model = modelSelect.value;
  607. GM_setValue('openrouter_model', model);
  608. });
  609.  
  610. // Save persona
  611. personaInput.addEventListener('change', () => {
  612. const persona = personaInput.value.trim();
  613. localStorage.setItem('cai_persona', persona);
  614. });
  615.  
  616. // Save input
  617. input.addEventListener('change', () => {
  618. const userInput = input.value.trim();
  619. localStorage.setItem('cai_input', userInput);
  620. });
  621.  
  622. // Generate text from manual input
  623. generateBtn.addEventListener('click', () => {
  624. const selection = apiSelection.value;
  625. const openRouterKey = apiKeyInput.value.trim();
  626. const geminiKey = geminiKeyInput.value.trim();
  627. const model = modelSelect.value;
  628. const persona = personaInput.value.trim();
  629. const userInput = input.value.trim();
  630. const temperature = geminiTempInput.value.trim();
  631.  
  632. if (selection === 'openrouter' && !openRouterKey) {
  633. output.value = 'Please enter your OpenRouter API key.';
  634. copyBtn.style.display = 'none';
  635. return;
  636. } else if (selection === 'gemini' && !geminiKey) {
  637. output.value = 'Please enter your Google Gemini API key.';
  638. copyBtn.style.display = 'none';
  639. return;
  640. }
  641. if (!persona) {
  642. output.value = 'Please enter a persona.';
  643. copyBtn.style.display = 'none';
  644. return;
  645. }
  646. if (!userInput) {
  647. output.value = 'Please enter a message.';
  648. copyBtn.style.display = 'none';
  649. return;
  650. }
  651.  
  652. const messages = [
  653. { tag: 'system', message: persona },
  654. { tag: 'user', message: userInput }
  655. ];
  656.  
  657. if (selection === 'openrouter') {
  658. generateOpenRouterText(openRouterKey, model, messages, output, copyBtn);
  659. } else if (selection === 'gemini') {
  660. generateGeminiText(geminiKey, model, messages, temperature, output, copyBtn);
  661. }
  662. });
  663.  
  664. // Generate next response from conversation history with new input
  665. generateNextBtn.addEventListener('click', async () => {
  666. const selection = apiSelection.value;
  667. const openRouterKey = apiKeyInput.value.trim();
  668. const geminiKey = geminiKeyInput.value.trim();
  669. const model = modelSelect.value;
  670. const persona = personaInput.value.trim();
  671. const userInput = input.value.trim();
  672. const temperature = geminiTempInput.value.trim();
  673.  
  674. if (selection === 'openrouter' && !openRouterKey) {
  675. output.value = 'Please enter your OpenRouter API key.';
  676. copyBtn.style.display = 'none';
  677. return;
  678. } else if (selection === 'gemini' && !geminiKey) {
  679. output.value = 'Please enter your Google Gemini API key.';
  680. copyBtn.style.display = 'none';
  681. return;
  682. }
  683. if (!persona) {
  684. output.value = 'Please enter a persona.';
  685. copyBtn.style.display = 'none';
  686. return;
  687. }
  688. if (!userInput) {
  689. output.value = 'Please enter a message.';
  690. copyBtn.style.display = 'none';
  691. return;
  692. }
  693.  
  694. const AccessToken = getAccessToken();
  695. if (!AccessToken) {
  696. output.value = 'Could not retrieve access token. Are you logged in?';
  697. copyBtn.style.display = 'none';
  698. return;
  699. }
  700.  
  701. const charId = getCharId();
  702. if (!charId) {
  703. output.value = 'Could not find character ID. Are you on a chat page?';
  704. copyBtn.style.display = 'none';
  705. return;
  706. }
  707.  
  708. const converId = await getCurrentConverId();
  709. if (!converId) {
  710. output.value = 'Could not find current conversation ID.';
  711. copyBtn.style.display = 'none';
  712. return;
  713. }
  714.  
  715. output.value = 'Fetching conversation history...';
  716. try {
  717. const chatData = await fetchMessagesChat2({ AccessToken, converExtId: converId });
  718. const messages = [
  719. { tag: 'system', message: persona },
  720. ...(chatData || []).map(msg => ({
  721. tag: msg.tag,
  722. message: msg.message
  723. })),
  724. { tag: 'user', message: userInput }
  725. ];
  726.  
  727. if (selection === 'openrouter') {
  728. generateOpenRouterText(openRouterKey, model, messages, output, copyBtn);
  729. } else if (selection === 'gemini') {
  730. generateGeminiText(geminiKey, model, messages, temperature, output, copyBtn);
  731. }
  732. } catch (error) {
  733. output.value = `Error: ${error.message}`;
  734. copyBtn.style.display = 'none';
  735. }
  736. });
  737.  
  738. // Copy output to clipboard
  739. copyBtn.addEventListener('click', () => {
  740. navigator.clipboard.writeText(output.value).then(() => {
  741. alert('Text copied to clipboard!');
  742. });
  743. });
  744.  
  745. // Update models list and dropdown based on API selection
  746. function updateModels(api) {
  747. if (api === 'openrouter') {
  748. const apiKey = apiKeyInput.value.trim();
  749. fetchOpenRouterModels(apiKey, (models, error) => {
  750. if (error) {
  751. modelsTableBody.innerHTML = `<tr><td colspan="3">${error}</td></tr>`;
  752. modelSelect.innerHTML = `<option value="${storedModel}">${storedModel} (default)</option>`;
  753. return;
  754. }
  755.  
  756. modelSelect.innerHTML = models.map(model => {
  757. const selected = model.id === storedModel ? 'selected' : '';
  758. return `<option value="${model.id}" ${selected}>${model.name}</option>`;
  759. }).join('');
  760.  
  761. modelsTableBody.innerHTML = models.map(model => {
  762. const inputCost = model.pricing?.prompt ? (parseFloat(model.pricing.prompt) * 1000).toFixed(4) : 'N/A';
  763. const outputCost = model.pricing?.completion ? (parseFloat(model.pricing.completion) * 1000).toFixed(4) : 'N/A';
  764. return `
  765. <tr>
  766. <td>${model.name}</td>
  767. <td>${inputCost}</td>
  768. <td>${outputCost}</td>
  769. </tr>
  770. `;
  771. }).join('');
  772. });
  773. } else if (api === 'gemini') {
  774. const apiKey = geminiKeyInput.value.trim();
  775. fetchGeminiModels(apiKey, (models, error) => {
  776. if (error) {
  777. modelsTableBody.innerHTML = `<tr><td colspan="3">${error}</td></tr>`;
  778. modelSelect.innerHTML = models.map(model => {
  779. const selected = model.id === storedModel ? 'selected' : '';
  780. return `<option value="${model.id}" ${selected}>${model.name}</option>`;
  781. }).join('');
  782. } else {
  783. modelSelect.innerHTML = models.map(model => {
  784. const selected = model.id === storedModel ? 'selected' : '';
  785. return `<option value="${model.id}" ${selected}>${model.name}</option>`;
  786. }).join('');
  787.  
  788. modelsTableBody.innerHTML = models.map(model => {
  789. return `
  790. <tr>
  791. <td>${model.name}</td>
  792. <td>${model.pricing.prompt}</td>
  793. <td>${model.pricing.completion}</td>
  794. </tr>
  795. `;
  796. }).join('');
  797. }
  798. });
  799. }
  800. }
  801.  
  802. // Initial models fetch based on stored selection
  803. updateModels(storedApiSelection);
  804. }
  805.  
  806. // ### Run Script
  807. waitForDOM(() => {
  808. console.log('DOM ready, initializing UI');
  809. createUI();
  810. });
  811. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址