Persona Impersonator

Impersonation. (RYW-Style)

// ==UserScript==
// @name         Persona Impersonator
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Impersonation. (RYW-Style)
// @author       Grok 3 (xAI)
// @match        https://character.ai/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @setting      openrouter_key    {type: 'text', default: '', description: 'Your OpenRouter API Key'}
// @setting      openrouter_model  {type: 'text', default: 'openai/gpt-3.5-turbo', description: 'OpenRouter Model (e.g., openai/gpt-3.5-turbo)'}
// ==/UserScript==

(function() {
    'use strict';

    // ### Helper Functions for Fetching Conversation History

    /** Wraps GM_xmlhttpRequest in a Promise for easier async handling */
    function GM_xmlhttpRequestPromise(details) {
        return new Promise((resolve, reject) => {
            details.onload = function(response) {
                resolve(response);
            };
            details.onerror = function() {
                reject(new Error('GM_xmlhttpRequest failed'));
            };
            GM_xmlhttpRequest(details);
        });
    }

    /** Retrieves the access token from a meta tag */
    function getAccessToken() {
        const meta = document.querySelector('meta[cai_token]');
        return meta ? meta.getAttribute('cai_token') : null;
    }

    /** Extracts the character ID from the URL */
    function getCharId() {
        const path = window.location.pathname.split('/');
        if (path[1] === 'chat' && path[2]) {
            return path[2];
        }
        return null;
    }

    /** Fetches the current conversation ID for the character */
    async function getCurrentConverId() {
        const AccessToken = getAccessToken();
        const charId = getCharId();
        if (!AccessToken || !charId) return null;

        try {
            const res = await GM_xmlhttpRequestPromise({
                method: "GET",
                url: `https://neo.character.ai/chats/recent/${charId}`,
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    "authorization": AccessToken
                }
            });

            if (res.status === 200) {
                const data = JSON.parse(res.responseText);
                if (data.chats && data.chats.length > 0) {
                    return data.chats[0].chat_id;
                }
            }
            return null;
        } catch (error) {
            console.error('Error fetching conversation ID:', error);
            return null;
        }
    }

    /** Fetches all messages in the conversation, handling pagination with custom tagging */
    async function fetchMessagesChat2({ AccessToken, converExtId, nextToken = null, turns = [] }) {
        let url = `https://neo.character.ai/turns/${converExtId}/`;
        if (nextToken) url += `?next_token=${nextToken}`;

        try {
            const res = await GM_xmlhttpRequestPromise({
                method: "GET",
                url: url,
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    "authorization": AccessToken
                }
            });

            if (res.status === 200) {
                const data = JSON.parse(res.responseText);
                turns = [...turns, ...data.turns];
                if (data.meta.next_token == null) {
                    const simplifiedChat = turns.map(msg => {
                        const primary = msg.candidates.find(c => c.candidate_id === msg.primary_candidate_id);
                        const [alternative] = msg.candidates.slice(-1);
                        const chosen = primary ?? alternative;
                        const senderName = msg.author.name || (msg.author.is_human ? 'Human' : 'Axel');
                        const tag = msg.author.is_human ? `user:${senderName}` : 'assistant';
                        return {
                            tag: tag,
                            message: chosen?.raw_content || "[Message broken]"
                        };
                    });
                    simplifiedChat.reverse(); // Oldest first
                    return simplifiedChat;
                } else {
                    return fetchMessagesChat2({ AccessToken, converExtId, nextToken: data.meta.next_token, turns });
                }
            } else {
                throw new Error(`Fetch failed: ${res.status}`);
            }
        } catch (error) {
            console.error('Error fetching messages:', error);
            throw error;
        }
    }

    // ### Core Functions

    /** Waits for DOM to be ready */
    function waitForDOM(callback) {
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            callback();
        } else {
            document.addEventListener('DOMContentLoaded', callback);
            const observer = new MutationObserver(() => {
                if (document.getElementId('__next')) {
                    observer.disconnect();
                    callback();
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }

    /** Fetches OpenRouter models and pricing */
    function fetchOpenRouterModels(apiKey, callback) {
        if (!apiKey) {
            callback(null, 'Please enter your OpenRouter API key to fetch models.');
            return;
        }

        console.log('Fetching OpenRouter models...');
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://openrouter.ai/api/v1/models',
            headers: {
                'Authorization': `Bearer ${apiKey}`
            },
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    callback(data.data, null);
                } else {
                    callback(null, `Error: ${response.status} - ${response.responseText}`);
                }
            },
            onerror: function() {
                callback(null, 'Failed to connect to OpenRouter API.');
            }
        });
    }

    /** Fetches Google Gemini models using the Generative Language API */
    function fetchGeminiModels(apiKey, callback) {
        if (!apiKey) {
            callback(null, 'Please enter your Google Gemini API key to fetch models.');
            return;
        }

        console.log('Fetching Google Gemini models...');
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey,
            headers: {
                'Content-Type': 'application/json'
            },
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const models = data.models.filter(model =>
                        model.name.startsWith('models/gemini-1.5-') ||
                        model.name.startsWith('models/gemini-2.0-') ||
                        model.name.startsWith('models/aqa') ||
                        model.name.startsWith('models/gemini-2.5-') ||
                        model.name.startsWith('models/gemini-embedding-') ||
                        model.name.startsWith('models/gemini-exp-') ||
                        model.name.startsWith('models/gemini-ultra') ||
                        model.name.startsWith('models/gemma-')
                    ).map(model => ({
                        id: model.name.split('/').pop(),
                        name: model.displayName || model.name.split('/').pop(),
                        pricing: { prompt: 'N/A', completion: 'N/A' }
                    }));
                    callback(models, null);
                } else {
                    console.error('Failed to fetch Gemini models:', response.status, response.responseText);
                    // Fallback to hardcoded list if API call fails
                    const fallbackModels = [
                        { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-1.0-pro-001', name: 'Gemini 1.0 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-1.5-pro-002', name: 'Gemini 1.5 Pro 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-1.5-pro-001', name: 'Gemini 1.5 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-1.5-flash-002', name: 'Gemini 1.5 Flash 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-1.5-flash-001', name: 'Gemini 1.5 Flash 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-2.0-pro-vision', name: 'Gemini 2.0 Pro Vision', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-2.0-nano', name: 'Gemini 2.0 Nano', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-exp-0827', name: 'Gemini Exp 0827', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemini-exp-0924', name: 'Gemini Exp 0924', pricing: { prompt: 'N/A', completion: 'N/A' } },
                        { id: 'gemma-3-27b-it', name: 'Gemma 3 27B IT', pricing: { prompt: 'N/A', completion: 'N/A' } }
                    ];
                    callback(fallbackModels, `Error: ${response.status} - ${response.responseText}`);
                }
            },
            onerror: function() {
                console.error('Network error fetching Gemini models');
                const fallbackModels = [
                    { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-1.0-pro-001', name: 'Gemini 1.0 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-1.5-pro-002', name: 'Gemini 1.5 Pro 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-1.5-pro-001', name: 'Gemini 1.5 Pro 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-1.5-flash-002', name: 'Gemini 1.5 Flash 002', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-1.5-flash-001', name: 'Gemini 1.5 Flash 001', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-2.0-pro-vision', name: 'Gemini 2.0 Pro Vision', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-2.0-nano', name: 'Gemini 2.0 Nano', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-exp-0827', name: 'Gemini Exp 0827', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemini-exp-0924', name: 'Gemini Exp 0924', pricing: { prompt: 'N/A', completion: 'N/A' } },
                    { id: 'gemma-3-27b-it', name: 'Gemma 3 27B IT', pricing: { prompt: 'N/A', completion: 'N/A' } }
                ];
                callback(fallbackModels, 'Failed to connect to Google Gemini API.');
            }
        });
    }

    /** Generates text using OpenRouter API */
    function generateOpenRouterText(apiKey, model, messages, output, copyBtn) {
        const payload = {
            model: model,
            messages: messages.map(msg => ({
                role: msg.tag.startsWith('user:') ? 'user' : msg.tag,
                content: msg.message
            }))
        };

        console.log('Sending request to OpenRouter:', payload);
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'https://openrouter.ai/api/v1/chat/completions',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            data: JSON.stringify(payload),
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const generatedText = data.choices[0].message.content.trim();
                    output.value = generatedText;
                    copyBtn.style.display = 'block';
                    console.log('Generated:', generatedText);
                } else {
                    output.value = `Error: ${response.status} - ${response.responseText}`;
                    copyBtn.style.display = 'none';
                }
            },
            onerror: function() {
                output.value = 'Failed to connect to OpenRouter API.';
                copyBtn.style.display = 'none';
            }
        });
    }

    /** Generates text using Google Gemini API with temperature */
    function generateGeminiText(apiKey, model, messages, temperature, output, copyBtn) {
        const payload = {
            contents: [{
                parts: messages.map(msg => ({
                    text: `${msg.tag}: ${msg.message}`
                }))
            }],
            generationConfig: {
                temperature: parseFloat(temperature) || 1.0,
                maxOutputTokens: 2048
            }
        };

        console.log('Sending request to Google Gemini:', payload);
        console.log(model, payload);
        GM_xmlhttpRequest({
            method: 'POST',
            url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
            headers: {
                'Content-Type': 'application/json'
            },
            data: JSON.stringify(payload),
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const generatedText = data.candidates[0].content.parts[0].text.trim();
                    output.value = generatedText;
                    copyBtn.style.display = 'block';
                    console.log('Generated:', generatedText);
                } else {
                    output.value = `Error: ${response.status} - ${response.responseText}`;
                    copyBtn.style.display = 'none';
                }
            },
            onerror: function() {
                output.value = 'Failed to connect to Google Gemini API.';
                copyBtn.style.display = 'none';
            }
        });
    }

    // ### UI Creation

    function createUI() {
        const storedPersona = localStorage.getItem('cai_persona') || '';
        const storedApiSelection = localStorage.getItem('cai_api_selection') || 'openrouter';
        const storedOpenRouterKey = GM_getValue('openrouter_key', '');
        const storedGeminiKey = localStorage.getItem('cai_gemini_key') || '';
        const storedModel = GM_getValue('openrouter_model', 'openai/gpt-3.5-turbo');
        const storedInput = localStorage.getItem('cai_input') || '';
        const storedTemperature = localStorage.getItem('cai_gemini_temperature') || '1.0';

        // Inject CSS
        const style = document.createElement('style');
        style.innerHTML = `
            @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
            .ptrk_main {
                position: fixed !important;
                display: flex;
                flex-direction: column;
                margin: 0;
                z-index: 10000 !important;
                min-width: 300px;
                background-color: rgba(33, 37, 41, 0.95);
                right: 0;
                top: 0;
                height: 100vh;
                padding: 18px;
                color: white;
                font-family: "Noto Sans", sans-serif;
                font-size: 13px;
                transition: transform 0.3s ease;
                width: 470px;
                box-sizing: border-box;
                box-shadow: -2px 0 5px rgba(0, 0, 0, 0.5);
                overflow-y: auto;
            }
            .ptrk_main.ptrk_hidden {
                transform: translateX(100%);
            }
            .ptrk_toggle_btn {
                position: fixed !important;
                top: 105px;
                right: 10px;
                z-index: 10001;
                background-color: rgba(33, 37, 41, 0.95);
                color: white;
                padding: 8px 16px;
                border-radius: 5px;
                cursor: pointer;
                font-family: "Noto Sans", sans-serif;
                font-size: 14px;
                transition: background-color 0.3s;
                box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
            }
            .ptrk_toggle_btn:hover {
                background-color: rgba(50, 55, 60, 0.95);
            }
            .ptrk_main fieldset {
                border: 1px solid rgb(59, 59, 63);
                border-radius: 3px;
                padding: 10px;
                margin-bottom: 10px;
            }
            .ptrk_main legend {
                font-size: 12px;
                padding: 0 5px;
            }
            .ptrk_main input, .ptrk_main textarea, .ptrk_main select {
                width: 100%;
                color: #d1d5db;
                padding: 10px;
                margin: 5px 0;
                box-sizing: border-box;
                font-size: 12px;
                background: rgba(0, 0, 0, 0.2);
                border: 1px solid #8e8e8e;
                border-radius: 3px;
                -webkit-appearance: none;
                -moz-appearance: none;
                appearance: none;
            }
            .ptrk_main select {
                cursor: pointer;
                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;
            }
            .ptrk_main select::-ms-expand {
                display: none;
            }
            .ptrk_main select option {
                color: #d1d5db;
                background: rgba(33, 37, 41, 0.95);
            }
            .ptrk_main textarea {
                resize: vertical;
            }
            .ptrk_main textarea[readonly] {
                background: rgba(255, 255, 255, 0.05);
                border-color: rgba(255, 255, 255, 0.1);
            }
            .ptrk_main .abtn {
                cursor: pointer;
                padding: 6px 12px;
                border-radius: 3px;
                font-weight: bold;
                margin: 2px;
                background: rgb(95, 99, 101);
                text-align: center;
                transition: background 0.2s;
            }
            .ptrk_main .abtn:hover {
                background: rgb(118, 123, 125);
            }
            .ptrk_main .midbtns {
                display: flex;
                justify-content: center;
                margin-top: 5px;
            }
            .ptrk_models_table {
                max-height: 200px;
                overflow-y: auto;
                margin-top: 5px;
            }
            .ptrk_models_table table {
                width: 100%;
                border-collapse: collapse;
                font-size: 11px;
                color: #d1d5db;
                background: rgba(0, 0, 0, 0.5);
            }
            .ptrk_models_table th, .ptrk_models_table td {
                border: 1px solid rgb(45, 45, 48);
                padding: 5px;
                text-align: left;
            }
            .ptrk_models_table th {
                background: rgba(0, 0, 0, 0.6);
            }
        `;
        document.head.appendChild(style);

        // Toggle button
        const toggleBtn = document.createElement('div');
        toggleBtn.classList.add('ptrk_toggle_btn');
        toggleBtn.textContent = 'Hide UI';
        document.body.appendChild(toggleBtn);

        // Main UI container
        const mainDom = document.createElement('div');
        mainDom.classList.add('ptrk_main');
        mainDom.innerHTML = `
            <fieldset>
                <legend>API Selection</legend>
                <select id="api-selection">
                    <option value="openrouter" ${storedApiSelection === 'openrouter' ? 'selected' : ''}>OpenRouter</option>
                    <option value="gemini" ${storedApiSelection === 'gemini' ? 'selected' : ''}>Google Gemini</option>
                </select>
            </fieldset>
            <fieldset id="openrouter-key-field" style="${storedApiSelection === 'openrouter' ? '' : 'display: none;'}">
                <legend>OpenRouter API Key</legend>
                <input id="api-key" type="text" placeholder="Enter your OpenRouter API key..." value="${storedOpenRouterKey}">
            </fieldset>
            <fieldset id="gemini-key-field" style="${storedApiSelection === 'gemini' ? '' : 'display: none;'}">
                <legend>Google Gemini API Key</legend>
                <input id="gemini-key" type="text" placeholder="Enter your Gemini API key..." value="${storedGeminiKey}">
            </fieldset>
            <fieldset id="gemini-temp-field" style="${storedApiSelection === 'gemini' ? '' : 'display: none;'}">
                <legend>Temperature (0.0 - 2.0)</legend>
                <input id="gemini-temperature" type="number" step="0.1" min="0" max="2" placeholder="Enter temperature (default 1.0)" value="${storedTemperature}">
            </fieldset>
            <fieldset>
                <legend>Model</legend>
                <select id="model-select">
                    <option value="${storedModel}">${storedModel} (default)</option>
                </select>
            </fieldset>
            <fieldset>
                <legend>Available Models</legend>
                <div class="ptrk_models_table">
                    <table>
                        <thead>
                            <tr>
                                <th>Model Name</th>
                                <th>Input ($/1k)</th>
                                <th>Output ($/1k)</th>
                            </tr>
                        </thead>
                        <tbody id="models-table-body">
                            <tr><td colspan="3">Loading models...</td></tr>
                        </tbody>
                    </table>
                </div>
            </fieldset>
            <fieldset>
                <legend>Persona</legend>
                <textarea id="persona-input" placeholder="Enter your persona here...">${storedPersona}</textarea>
            </fieldset>
            <fieldset>
                <legend>Input</legend>
                <textarea id="user-input" placeholder="Enter message to impersonate...">${storedInput}</textarea>
                <div class="midbtns">
                    <div class="abtn" data-tag="generate">Generate Text</div>
                    <div class="abtn" data-tag="generate-next">Generate Next Response</div>
                </div>
            </fieldset>
            <fieldset>
                <legend>Output</legend>
                <textarea id="output-text" readonly></textarea>
                <div class="midbtns">
                    <div class="abtn" data-tag="copy" style="display: none;">Copy to Clipboard</div>
                </div>
            </fieldset>
        `;
        document.body.appendChild(mainDom);

        // UI elements
        const apiSelection = mainDom.querySelector('#api-selection');
        const openRouterKeyField = mainDom.querySelector('#openrouter-key-field');
        const geminiKeyField = mainDom.querySelector('#gemini-key-field');
        const geminiTempField = mainDom.querySelector('#gemini-temp-field');
        const apiKeyInput = mainDom.querySelector('#api-key');
        const geminiKeyInput = mainDom.querySelector('#gemini-key');
        const geminiTempInput = mainDom.querySelector('#gemini-temperature');
        const modelSelect = mainDom.querySelector('#model-select');
        const modelsTableBody = mainDom.querySelector('#models-table-body');
        const personaInput = mainDom.querySelector('#persona-input');
        const input = mainDom.querySelector('#user-input');
        const generateBtn = mainDom.querySelector('[data-tag="generate"]');
        const generateNextBtn = mainDom.querySelector('[data-tag="generate-next"]');
        const output = mainDom.querySelector('#output-text');
        const copyBtn = mainDom.querySelector('[data-tag="copy"]');

        // Toggle UI visibility
        let isHidden = false;
        toggleBtn.addEventListener('click', () => {
            isHidden = !isHidden;
            mainDom.classList.toggle('ptrk_hidden', isHidden);
            toggleBtn.textContent = isHidden ? 'Show UI' : 'Hide UI';
        });

        // API selection change
        apiSelection.addEventListener('change', () => {
            const selection = apiSelection.value;
            localStorage.setItem('cai_api_selection', selection);
            if (selection === 'openrouter') {
                openRouterKeyField.style.display = '';
                geminiKeyField.style.display = 'none';
                geminiTempField.style.display = 'none';
                updateModels('openrouter');
            } else if (selection === 'gemini') {
                openRouterKeyField.style.display = 'none';
                geminiKeyField.style.display = '';
                geminiTempField.style.display = '';
                updateModels('gemini');
            }
        });

        // Save OpenRouter API key
        apiKeyInput.addEventListener('change', () => {
            const apiKey = apiKeyInput.value.trim();
            GM_setValue('openrouter_key', apiKey);
            if (apiSelection.value === 'openrouter') {
                updateModels('openrouter');
            }
        });

        // Save Gemini API key
        geminiKeyInput.addEventListener('change', () => {
            const apiKey = geminiKeyInput.value.trim();
            localStorage.setItem('cai_gemini_key', apiKey);
            if (apiSelection.value === 'gemini') {
                updateModels('gemini');
            }
        });

        // Save Gemini temperature
        geminiTempInput.addEventListener('change', () => {
            const temperature = geminiTempInput.value.trim();
            localStorage.setItem('cai_gemini_temperature', temperature);
        });

        // Save selected model
        modelSelect.addEventListener('change', () => {
            const model = modelSelect.value;
            GM_setValue('openrouter_model', model);
        });

        // Save persona
        personaInput.addEventListener('change', () => {
            const persona = personaInput.value.trim();
            localStorage.setItem('cai_persona', persona);
        });

        // Save input
        input.addEventListener('change', () => {
            const userInput = input.value.trim();
            localStorage.setItem('cai_input', userInput);
        });

        // Generate text from manual input
        generateBtn.addEventListener('click', () => {
            const selection = apiSelection.value;
            const openRouterKey = apiKeyInput.value.trim();
            const geminiKey = geminiKeyInput.value.trim();
            const model = modelSelect.value;
            const persona = personaInput.value.trim();
            const userInput = input.value.trim();
            const temperature = geminiTempInput.value.trim();

            if (selection === 'openrouter' && !openRouterKey) {
                output.value = 'Please enter your OpenRouter API key.';
                copyBtn.style.display = 'none';
                return;
            } else if (selection === 'gemini' && !geminiKey) {
                output.value = 'Please enter your Google Gemini API key.';
                copyBtn.style.display = 'none';
                return;
            }
            if (!persona) {
                output.value = 'Please enter a persona.';
                copyBtn.style.display = 'none';
                return;
            }
            if (!userInput) {
                output.value = 'Please enter a message.';
                copyBtn.style.display = 'none';
                return;
            }

            const messages = [
                { tag: 'system', message: persona },
                { tag: 'user', message: userInput }
            ];

            if (selection === 'openrouter') {
                generateOpenRouterText(openRouterKey, model, messages, output, copyBtn);
            } else if (selection === 'gemini') {
                generateGeminiText(geminiKey, model, messages, temperature, output, copyBtn);
            }
        });

        // Generate next response from conversation history with new input
        generateNextBtn.addEventListener('click', async () => {
            const selection = apiSelection.value;
            const openRouterKey = apiKeyInput.value.trim();
            const geminiKey = geminiKeyInput.value.trim();
            const model = modelSelect.value;
            const persona = personaInput.value.trim();
            const userInput = input.value.trim();
            const temperature = geminiTempInput.value.trim();

            if (selection === 'openrouter' && !openRouterKey) {
                output.value = 'Please enter your OpenRouter API key.';
                copyBtn.style.display = 'none';
                return;
            } else if (selection === 'gemini' && !geminiKey) {
                output.value = 'Please enter your Google Gemini API key.';
                copyBtn.style.display = 'none';
                return;
            }
            if (!persona) {
                output.value = 'Please enter a persona.';
                copyBtn.style.display = 'none';
                return;
            }
            if (!userInput) {
                output.value = 'Please enter a message.';
                copyBtn.style.display = 'none';
                return;
            }

            const AccessToken = getAccessToken();
            if (!AccessToken) {
                output.value = 'Could not retrieve access token. Are you logged in?';
                copyBtn.style.display = 'none';
                return;
            }

            const charId = getCharId();
            if (!charId) {
                output.value = 'Could not find character ID. Are you on a chat page?';
                copyBtn.style.display = 'none';
                return;
            }

            const converId = await getCurrentConverId();
            if (!converId) {
                output.value = 'Could not find current conversation ID.';
                copyBtn.style.display = 'none';
                return;
            }

            output.value = 'Fetching conversation history...';
            try {
                const chatData = await fetchMessagesChat2({ AccessToken, converExtId: converId });
                const messages = [
                    { tag: 'system', message: persona },
                    ...(chatData || []).map(msg => ({
                        tag: msg.tag,
                        message: msg.message
                    })),
                    { tag: 'user', message: userInput }
                ];

                if (selection === 'openrouter') {
                    generateOpenRouterText(openRouterKey, model, messages, output, copyBtn);
                } else if (selection === 'gemini') {
                    generateGeminiText(geminiKey, model, messages, temperature, output, copyBtn);
                }
            } catch (error) {
                output.value = `Error: ${error.message}`;
                copyBtn.style.display = 'none';
            }
        });

        // Copy output to clipboard
        copyBtn.addEventListener('click', () => {
            navigator.clipboard.writeText(output.value).then(() => {
                alert('Text copied to clipboard!');
            });
        });

        // Update models list and dropdown based on API selection
        function updateModels(api) {
            if (api === 'openrouter') {
                const apiKey = apiKeyInput.value.trim();
                fetchOpenRouterModels(apiKey, (models, error) => {
                    if (error) {
                        modelsTableBody.innerHTML = `<tr><td colspan="3">${error}</td></tr>`;
                        modelSelect.innerHTML = `<option value="${storedModel}">${storedModel} (default)</option>`;
                        return;
                    }

                    modelSelect.innerHTML = models.map(model => {
                        const selected = model.id === storedModel ? 'selected' : '';
                        return `<option value="${model.id}" ${selected}>${model.name}</option>`;
                    }).join('');

                    modelsTableBody.innerHTML = models.map(model => {
                        const inputCost = model.pricing?.prompt ? (parseFloat(model.pricing.prompt) * 1000).toFixed(4) : 'N/A';
                        const outputCost = model.pricing?.completion ? (parseFloat(model.pricing.completion) * 1000).toFixed(4) : 'N/A';
                        return `
                            <tr>
                                <td>${model.name}</td>
                                <td>${inputCost}</td>
                                <td>${outputCost}</td>
                            </tr>
                        `;
                    }).join('');
                });
            } else if (api === 'gemini') {
                const apiKey = geminiKeyInput.value.trim();
                fetchGeminiModels(apiKey, (models, error) => {
                    if (error) {
                        modelsTableBody.innerHTML = `<tr><td colspan="3">${error}</td></tr>`;
                        modelSelect.innerHTML = models.map(model => {
                            const selected = model.id === storedModel ? 'selected' : '';
                            return `<option value="${model.id}" ${selected}>${model.name}</option>`;
                        }).join('');
                    } else {
                        modelSelect.innerHTML = models.map(model => {
                            const selected = model.id === storedModel ? 'selected' : '';
                            return `<option value="${model.id}" ${selected}>${model.name}</option>`;
                        }).join('');

                        modelsTableBody.innerHTML = models.map(model => {
                            return `
                                <tr>
                                    <td>${model.name}</td>
                                    <td>${model.pricing.prompt}</td>
                                    <td>${model.pricing.completion}</td>
                                </tr>
                            `;
                        }).join('');
                    }
                });
            }
        }

        // Initial models fetch based on stored selection
        updateModels(storedApiSelection);
    }

    // ### Run Script
    waitForDOM(() => {
        console.log('DOM ready, initializing UI');
        createUI();
    });
})();

QingJ © 2025

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