Native Notepad for c.ai

Adds a minimal autosaving notepad to the right side panel, below the character name/PFP. One persistent note per character.

// ==UserScript==
// @name         Native Notepad for c.ai
// @namespace    http://fakesite.net/minimalnotepad
// @version      0.1
// @description  Adds a minimal autosaving notepad to the right side panel, below the character name/PFP. One persistent note per character.
// @author       Mr005K
// @match        https://character.ai/chat/*
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    let currentCharId = getCharId();

    function getCharId() {
        const path = window.location.pathname;
        if (path.startsWith('/chat/')) {
            return path.split('/chat/')[1].split('/')[0];
        }
        return '';
    }

    const observer = new MutationObserver(() => {
        const panel = document.querySelector('div[role="dialog"][data-state="open"]');
        if (!panel) return;

        const header = panel.querySelector('div.flex.gap-3');
        if (!header) return;

        const newCharId = getCharId();
        if (!newCharId) return;

        let noteDiv = panel.querySelector('#cai-notepad');
        if (!noteDiv) {
            noteDiv = document.createElement('div');
            noteDiv.id = 'cai-notepad';
            noteDiv.className = 'mt-4';

            const textarea = document.createElement('textarea');
            textarea.className = 'w-full h-32 p-2 bg-background border border-border-outline rounded-md text-foreground resize-vertical';
            textarea.value = GM_getValue('cai_note_' + newCharId, '');

            let saveTimeout;
            textarea.addEventListener('input', () => {
                clearTimeout(saveTimeout);
                saveTimeout = setTimeout(() => {
                    GM_setValue('cai_note_' + newCharId, textarea.value);
                }, 1000);
            });

            noteDiv.appendChild(textarea);
            header.after(noteDiv);
        } else if (newCharId !== currentCharId) {
            const textarea = noteDiv.querySelector('textarea');
            if (textarea) {
                textarea.value = GM_getValue('cai_note_' + newCharId, '');
            }
        }

        currentCharId = newCharId;
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // To handle SPA navigation without mutations
    let lastPath = window.location.pathname;
    setInterval(() => {
        if (window.location.pathname !== lastPath) {
            lastPath = window.location.pathname;
            // Trigger a manual check
            observer.takeRecords(); // Clear queue
            observer.disconnect();
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }, 500);

})();