Persona Impersonator

Impersonation. (RYW-Style)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
    });
})();