Notepad For Character.ai

A notepad so fancy it should be a paid SaaS

// ==UserScript==
// @name         Notepad For Character.ai
// @namespace    http://tampermonkey.net/
// @version      9.3.0
// @description  A notepad so fancy it should be a paid SaaS
// @author       Mr005K via ChatGPT
// @match        https://character.ai/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ------------------------ Runtime Loader ------------------------
  function loadScript(url) {
    return new Promise((resolve, reject) => {
      const s = document.createElement('script');
      s.src = url;
      s.async = true;
      s.onload = resolve;
      s.onerror = () => reject(new Error(`Failed: ${url}`));
      document.head.appendChild(s);
    });
  }

  async function loadLibraries() {
    const libs = [
      {
        name: 'marked',
        check: () => window.marked,
        urls: [
          'https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js',
          'https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js',
          'https://unpkg.com/[email protected]/marked.min.js'
        ]
      },
      {
        name: 'DOMPurify',
        check: () => window.DOMPurify,
        urls: [
          'https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js',
          'https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.2/purify.min.js',
          'https://unpkg.com/[email protected]/dist/purify.min.js'
        ]
      },
      {
        name: 'LZString',
        check: () => window.LZString,
        urls: [
          'https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js',
          'https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js',
          'https://unpkg.com/[email protected]/libs/lz-string.min.js'
        ]
      },
      {
        name: 'twemoji',
        check: () => window.twemoji,
        urls: [
          'https://cdn.jsdelivr.net/npm/[email protected]/dist/twemoji.min.js',
          'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/twemoji.min.js',
          'https://unpkg.com/[email protected]/dist/twemoji.min.js'
        ]
      }
    ];

    for (const lib of libs) {
      if (lib.check()) continue; // already present
      let loaded = false;
      for (const url of lib.urls) {
        try {
          await loadScript(url);
          if (lib.check()) { loaded = true; break; }
        } catch (e) {
          // try next
        }
      }
      if (!loaded) {
        throw new Error(`Could not load ${lib.name} from any CDN.`);
      }
    }
  }

  // Boot once libs are ready
  loadLibraries().then(initApp).catch(err => {
    console.error('[GodMode Notepad] Failed to load libs:', err);
    alert('GodMode Notepad: Failed to load required libraries. Check console.');
  });

  // ------------------------ Main App ------------------------
  function initApp() {

    // ------------------------ Shadow DOM Wrapper ------------------------
    const container = document.createElement('div');
    container.id = 'godmode-notepad-root';
    document.documentElement.appendChild(container);
    const shadow = container.attachShadow({ mode: 'open' });

    // ------------------------ CSS ------------------------
    const style = document.createElement('style');
    style.textContent = `
      :host {
        all: initial;
        font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
      }
      :root, [data-theme="dark"] {
        --notepad-blur: 18px;
        --notepad-border: rgba(255,255,255,0.3);
        --notepad-glow: 0 8px 32px 0 rgba(31, 38, 135, 0.28);
        --notepad-radius: 1.5rem;
        --notepad-color-main: #ff2e63;
        --notepad-color-bg: #212733dd;
        --notepad-color-text: #f5f6fa;
        --notepad-color-btn: #00f5d4;
        --notepad-color-btn-txt: #192a56;
        --notepad-color-accent: #111;
        --notepad-transition: .18s cubic-bezier(.85,0,.15,1);
        --notepad-shadow: 0 4px 28px 6px rgba(31,38,135,0.15);
        --mark-bg: #ffff99;
        --mark-fg: #333;
      }
      [data-theme="light"] {
        --notepad-color-bg: #f0f0f8ee;
        --notepad-color-text: #1d1d1f;
        --notepad-border: rgba(0,0,0,0.25);
        --notepad-glow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
        --notepad-color-btn: #0044ff;
        --notepad-color-btn-txt: #fff;
        --notepad-color-main: #ff006e;
        --mark-bg: #ffe680;
        --mark-fg: #000;
      }
      [data-theme="neon"] {
        --notepad-color-bg: #090915f0;
        --notepad-color-text: #d1fff9;
        --notepad-border: rgba(0,255,213,0.4);
        --notepad-glow: 0 0 18px rgba(0,255,213,0.6);
        --notepad-color-btn: #ff00ff;
        --notepad-color-btn-txt: #000;
        --notepad-color-main: #00f5d4;
        --mark-bg: #0ff;
        --mark-fg: #000;
      }
      [data-theme="colorblind"] {
        --notepad-color-bg: #2a2a2aee;
        --notepad-color-text: #f0f0f0;
        --notepad-border: #bcbcbc;
        --notepad-color-btn: #ffcc00;
        --notepad-color-btn-txt: #000;
        --notepad-color-main: #00bcd4;
        --mark-bg: #ff0;
        --mark-fg: #000;
      }
      .notepad-superpanel {
        position: fixed; z-index: 999999;
        top: 0; right: 0;
        width: 460px; max-width: 90vw; height: 100vh;
        background: var(--notepad-color-bg);
        color: var(--notepad-color-text);
        border-left: 2.5px solid var(--notepad-border);
        border-radius: var(--notepad-radius) 0 0 var(--notepad-radius);
        box-shadow: var(--notepad-glow), var(--notepad-shadow);
        backdrop-filter: blur(var(--notepad-blur));
        transition: transform var(--notepad-transition), box-shadow var(--notepad-transition), opacity .25s;
        transform: translateX(110%);
        display: flex; flex-direction: column;
        overflow: hidden; user-select: text;
      }
      .notepad-superpanel.open { transform: translateX(0); box-shadow: 0 6px 48px 8px #0ff2; }
      .notepad-superpanel.overlay {
        width: 80%; height: 80%;
        border-radius: 1.5rem;
        border: 2.5px solid var(--notepad-border);
        left: 50%; top: 50%;
        transform: translate(-50%, -50%) scale(0.92);
        opacity: 0;
      }
      .notepad-superpanel.overlay.open {
        transform: translate(-50%, -50%) scale(1);
        opacity: 1;
      }
      .notepad-superpanel.shrunk { width: 320px; }
      .notepad-superpanel.floating { top:6vh; right:6vw; border-radius:2rem; height: 80vh; }
      .notepad-superpanel.left { left: 0; right: auto; border-radius: 0 var(--notepad-radius) var(--notepad-radius) 0; border-left:none; border-right: 2.5px solid var(--notepad-border); }
      .notepad-header {
        display:flex; align-items:center; gap:1rem; justify-content:space-between;
        padding:1.2rem 1.4rem 1.2rem 1.1rem;
        background:linear-gradient(110deg,rgba(0,0,0,0.13) 80%, var(--notepad-color-main) 120%);
        font-size:1.2rem; font-weight:700; letter-spacing: 0.01em;
        user-select: none; cursor: move;
      }
      .notepad-controls {display:flex;gap:0.7rem;flex-wrap:wrap;}
      .notepad-content {flex:1;display:flex;flex-direction:row;gap:0.4rem;overflow:hidden;}
      .notepad-editor-pane,.notepad-preview-pane{
        flex:1; min-width:0; min-height:0;
        display:flex; flex-direction:column; background:transparent; overflow:auto; padding:1rem 0.8rem;
        transition:background var(--notepad-transition);
      }
      .notepad-editor-pane {
        background:rgba(35,35,40,0.2); border-radius:1.1rem 0 0 1.1rem;
        border-right:1.5px solid rgba(255,255,255,0.09);
        position:relative;
      }
      .notepad-preview-pane {
        background:rgba(60,80,80,0.14); border-radius:0 1.1rem 1.1rem 0;
        border-left:1.5px solid rgba(255,255,255,0.09);
      }
      .notepad-textarea {
        width:100%;height:100%;resize:none;border:none;outline:none;background:transparent;color:var(--notepad-color-text);
        font-size:1.05rem;line-height:1.7;font-family:inherit;box-shadow:none;
      }
      .notepad-markdown-preview {
        width:100%;min-height:100px; color:var(--notepad-color-text);
        font-size:1.08rem; line-height:1.7;
      }
      .notepad-footer {
        padding:0.9rem 1.4rem; display:flex;align-items:center;justify-content:space-between; flex-wrap:wrap;
        background:rgba(30,40,60,0.17);
        border-top: 1.5px solid var(--notepad-border);
        font-size:0.95rem;user-select: none;
        gap:0.4rem;
      }
      .notepad-theme-selector, .notepad-btn, .notepad-note-selector, .notepad-search {
        border:none; border-radius:0.7rem; outline:none;
        padding:0.38rem 1.1rem; font-weight:600;
        background:var(--notepad-color-btn);
        color:var(--notepad-color-btn-txt);
        cursor:pointer; transition:filter .17s;
      }
      .notepad-note-selector, .notepad-search { background:#333;color:#fff;padding:0.3rem 0.8rem; }
      .notepad-btn:active { filter:brightness(0.85);}
      .notepad-btn.save { background:var(--notepad-color-main);color:white;}
      .notepad-btn.emoji { padding:0.3rem 0.7rem;font-size:1.15rem; }
      .notepad-btn.shrink { background:#1f1f1faa;color:#00f5d4;}
      .notepad-btn.float { background:#00f5d430;color:#ff2e63;}
      .notepad-btn.left { background:#1919a3cc;color:white;}
      .notepad-btn.split { background:#212733;color:#fff;}
      .notepad-btn.help { background:#fff;color:#222;}
      .notepad-btn.mode { background:#ffcc00;color:#333;}
      .notepad-btn.fs { background:#66ccff;color:#333;}
      .notepad-btn.del {background:#e74c3c;color:#fff;}
      .notepad-btn.export, .notepad-btn.import, .notepad-btn.new, .notepad-btn.dup, .notepad-btn.rename, .notepad-btn.history, .notepad-btn.restore { font-size:0.92rem;}
      .notepad-shortcut-badge {
        background:#333;color:#fff;border-radius:0.6rem;padding:0.12rem 0.5rem;font-size:0.75em;margin-left:0.25rem;
      }
      .notepad-toast {
        position:fixed;top:9vh;right:6vw;z-index:999999;
        background:rgba(30,30,30,0.92);color:#fffa;padding:1rem 1.7rem;border-radius:1.2rem;font-size:1.08rem;font-weight:500;box-shadow:0 8px 32px #00f5d430;pointer-events:none;animation:fadeIn 0.22s cubic-bezier(.85,0,.15,1);
      }
      @keyframes fadeIn{0%{opacity:0;transform:translateY(-8px);}100%{opacity:1;}}
      .notepad-unsaved { animation: pulse 1.2s infinite alternate; }
      @keyframes pulse { to { filter: brightness(1.3) drop-shadow(0 0 8px #ff2e63) } }
      .notepad-flag-bar { height:5px;width:100%;background: linear-gradient(90deg,#FF9933 33%,#FFF 33% 66%,#138808 66%); margin-bottom:-4px; }
      .notepad-emoji-picker {position:absolute;bottom:2.2rem;left:1rem;z-index:5;background:#222c;border-radius:1.1rem;padding:0.45rem 0.7rem;display:grid;grid-template-columns:repeat(7,1fr);gap:0.33rem;box-shadow:0 2px 12px #0003;}
      .notepad-emoji {font-size:1.28rem;cursor:pointer;padding:0.22rem;border-radius:0.4rem;transition:background .12s;}
      .notepad-emoji:hover {background:rgba(0,245,212,0.2);}
      mark { background: var(--mark-bg); color: var(--mark-fg); }
      .toggle-button {
        position:fixed;top:14px;right:17px;z-index:999998;
        background:var(--notepad-color-main);color:#fff;border:none;border-radius:1.3rem;
        padding:0.66rem 1.8rem;font-weight:700;font-size:1.13rem;box-shadow:0 2px 20px #ff2e6323;
        cursor:pointer;transition:filter .14s;
      }
      .notepad-history-list {
        max-height: 50vh; overflow:auto; background:#111c;color:#fff;padding:0.8rem;border-radius:0.7rem;margin-top:0.6rem;
      }
      .notepad-history-item {
        display:flex;justify-content:space-between;align-items:center;gap:0.6rem;background:#222a;padding:0.35rem 0.5rem;border-radius:0.5rem;margin-bottom:0.35rem;font-size:0.88rem;
      }
      .notepad-history-item button {
        padding:0.2rem 0.5rem;border-radius:0.4rem;background:var(--notepad-color-btn);color:var(--notepad-color-btn-txt);border:none;cursor:pointer;font-size:0.8rem;
      }
      .notepad-modal {
        position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:9999999;
      }
      .notepad-modal-box {
        background:#222e;color:#fff;padding:1rem 1.4rem;border-radius:1rem;min-width:300px;max-width:80vw;max-height:80vh;overflow:auto;box-shadow:0 4px 28px #0008;
      }
      .notepad-modal-box h3 { margin-top:0;margin-bottom:0.7rem;font-size:1.2rem;}
      .notepad-modal-box input[type="text"] {
        width:100%;padding:0.5rem;border-radius:0.5rem;border:none;margin:0.5rem 0;background:#333;color:#fff;
      }
      .notepad-modal-buttons {display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.9rem;}
      .notepad-modal-buttons button { padding:0.45rem 0.9rem;border:none;border-radius:0.5rem;cursor:pointer;font-weight:600; }
      .np-btn-ok { background:var(--notepad-color-btn);color:var(--notepad-color-btn-txt);}
      .np-btn-cancel { background:#666;color:#fff;}
    `;
    shadow.appendChild(style);

    // ------------------------ HTML ------------------------
    const panel = document.createElement('div');
    panel.className = 'notepad-superpanel side';
    panel.setAttribute('data-theme', localStorage.getItem('notepad_theme') || 'dark');

    const flagbar = document.createElement('div');
    flagbar.className = 'notepad-flag-bar';

    const header = document.createElement('div');
    header.className = 'notepad-header';
    header.innerHTML = `<span>✍️ Character.AI Notepad <span class="notepad-shortcut-badge">Ctrl+Shift+N</span></span>`;

    const noteSelector = document.createElement('select');
    noteSelector.className = 'notepad-note-selector';
    header.appendChild(noteSelector);

    const controls = document.createElement('div');
    controls.className = 'notepad-controls';
    header.appendChild(controls);

    const mkBtn = (cls, txt, title) => {
      const b = document.createElement('button');
      b.className = `notepad-btn ${cls}`;
      b.textContent = txt;
      if (title) b.title = title;
      return b;
    };

    const shrinkBtn = mkBtn('shrink', '⇔', 'Shrink Panel');
    const floatBtn = mkBtn('float', '↗', 'Float Panel');
    const leftBtn = mkBtn('left', '⇤', 'Stick Left');
    const splitBtn = mkBtn('split', '⎚', 'Toggle Split View');
    const helpBtn = mkBtn('help', '?', 'Help / Shortcuts');
    const modeBtn = mkBtn('mode', '↔', 'Side/Overlay Mode');
    const fsBtn = mkBtn('fs', '⛶', 'Full Screen');

    controls.append(shrinkBtn, floatBtn, leftBtn, splitBtn, helpBtn, modeBtn, fsBtn);

    const content = document.createElement('div');
    content.className = 'notepad-content';

    const editorPane = document.createElement('div');
    editorPane.className = 'notepad-editor-pane';

    const textarea = document.createElement('textarea');
    textarea.className = 'notepad-textarea';
    textarea.spellcheck = true;
    textarea.autocorrect = 'on';

    const emojiBtn = mkBtn('emoji', '😊', 'Emoji Picker');
    let emojiPicker = null;

    const previewPane = document.createElement('div');
    previewPane.className = 'notepad-preview-pane';

    const mdPreview = document.createElement('div');
    mdPreview.className = 'notepad-markdown-preview';

    previewPane.appendChild(mdPreview);
    editorPane.append(textarea, emojiBtn);
    content.append(editorPane, previewPane);

    const footer = document.createElement('div');
    footer.className = 'notepad-footer';

    const searchInput = document.createElement('input');
    searchInput.type = 'text';
    searchInput.placeholder = 'Search notes...';
    searchInput.className = 'notepad-search';

    const saveBtn = mkBtn('save', '💾 Save (Ctrl+S)');
    const newBtn = mkBtn('new', '➕ New');
    const dupBtn = mkBtn('dup', '📄 Duplicate');
    const renameBtn = mkBtn('rename', '✏️ Rename');
    const delBtn = mkBtn('del', '🗑 Delete');
    const expBtn = mkBtn('export', '⭳ Export');
    const impBtn = mkBtn('import', '⭱ Import');
    const histBtn = mkBtn('history', '🕒 History');

    const themeSelector = document.createElement('select');
    themeSelector.className = 'notepad-theme-selector';
    themeSelector.innerHTML = `
      <option value="dark">🌒 Dark</option>
      <option value="light">☀️ Light</option>
      <option value="neon">🌈 Neon</option>
      <option value="colorblind">🦉 Colorblind</option>
    `;

    const statBox = document.createElement('span');
    statBox.style.fontSize = '0.95em';

    footer.append(
      searchInput, saveBtn, newBtn, dupBtn, renameBtn, delBtn,
      expBtn, impBtn, histBtn, themeSelector, statBox
    );

    panel.append(flagbar, header, content, footer);
    shadow.appendChild(panel);

    const tglBtn = document.createElement('button');
    tglBtn.className = 'toggle-button';
    tglBtn.textContent = '📝 Notepad';
    shadow.appendChild(tglBtn);

    // ------------------------ Util ------------------------
    function debounce(func, wait) {
      let timeout;
      return function (...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    }

    function getCharacterId() {
      const parts = location.pathname.split('/');
      if (parts.includes('chat')) {
        const idx = parts.indexOf('chat');
        return parts[idx + 1] || null;
      }
      return null;
    }

    function noteKey(cid) { return `charai_supernotepad_${cid || 'global'}`; }
    function histKey(cid) { return `charai_supernotepad_history_${cid || 'global'}`; }

    function compress(obj) {
      return LZString.compressToUTF16(JSON.stringify(obj));
    }
    function decompress(str) {
      if (!str) return null;
      try {
        return JSON.parse(LZString.decompressFromUTF16(str) || 'null');
      } catch (e) {
        return null;
      }
    }

    function saveNotes(cid, notesData) {
      localStorage.setItem(noteKey(cid), compress(notesData));
    }
    function loadNotes(cid) {
      let raw = localStorage.getItem(noteKey(cid));
      let data = decompress(raw);
      if (!data || !data.notes) data = { notes: [] };
      return data;
    }

    function saveHistory(cid, snapshots) {
      localStorage.setItem(histKey(cid), compress(snapshots));
    }
    function loadHistory(cid) {
      let raw = localStorage.getItem(histKey(cid));
      let data = decompress(raw);
      if (!data || !data.snaps) data = { snaps: [] };
      return data;
    }

    function showToast(msg, time = 2200) {
      let t = document.createElement('div');
      t.className = 'notepad-toast';
      t.textContent = msg;
      shadow.appendChild(t);
      setTimeout(() => { t.remove(); }, time);
    }

    function downloadFile(name, content, type = 'text/plain') {
      let a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([content], { type }));
      a.download = name;
      shadow.appendChild(a);
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 3000);
      a.remove();
    }

    function uploadFile(cb) {
      let i = document.createElement('input');
      i.type = 'file';
      i.accept = '.txt,.md,.json';
      i.onchange = (e) => {
        let file = e.target.files[0];
        if (file) {
          let r = new FileReader();
          r.onload = () => cb(r.result, file.name);
          r.readAsText(file);
        }
      };
      shadow.appendChild(i);
      i.click();
      i.remove();
    }

    function setTheme(theme) {
      panel.setAttribute('data-theme', theme);
      localStorage.setItem('notepad_theme', theme);
    }

    function createModal(html, onOk, onCancel) {
      const modal = document.createElement('div');
      modal.className = 'notepad-modal';
      const box = document.createElement('div');
      box.className = 'notepad-modal-box';
      box.innerHTML = html;
      const btns = document.createElement('div');
      btns.className = 'notepad-modal-buttons';
      const ok = document.createElement('button');
      ok.textContent = 'OK';
      ok.className = 'np-btn-ok';
      const cancel = document.createElement('button');
      cancel.textContent = 'Cancel';
      cancel.className = 'np-btn-cancel';
      btns.append(ok, cancel);
      box.appendChild(btns);
      modal.appendChild(box);
      shadow.appendChild(modal);

      ok.onclick = () => { if (onOk) onOk(modal); modal.remove(); };
      cancel.onclick = () => { if (onCancel) onCancel(modal); modal.remove(); };

      return modal;
    }

    // ------------------------ State ------------------------
    let noteState = {
      charId: getCharacterId() || 'global',
      notes: [],
      currentNoteIndex: 0,
      split: true,
      dirty: false,
      searchQuery: '',
      history: { snaps: [] }
    };

    // ------------------------ Core Logic ------------------------
    function loadCurrentNote() {
      noteState.charId = getCharacterId() || 'global';
      let data = loadNotes(noteState.charId);
      noteState.notes = data.notes || [];
      noteState.split = data.split !== undefined ? data.split : true;
      if (noteState.notes.length === 0) {
        noteState.notes.push({ title: 'Note 1', content: '', ts: Date.now() });
      }
      noteState.currentNoteIndex = Math.min(noteState.currentNoteIndex, noteState.notes.length - 1);
      updateNoteSelector();
      loadSelectedNote();
      noteState.history = loadHistory(noteState.charId);
    }

    function loadSelectedNote() {
      let note = noteState.notes[noteState.currentNoteIndex];
      textarea.value = note.content;
      updatePreview();
      updateStats();
    }

    function updateNoteSelector() {
      noteSelector.innerHTML = '';
      noteState.notes.forEach((note, index) => {
        let option = document.createElement('option');
        option.value = index;
        option.textContent = note.title;
        noteSelector.appendChild(option);
      });
      noteSelector.value = noteState.currentNoteIndex;
    }

    function saveCurrentNote(showToastMsg = true) {
      let note = noteState.notes[noteState.currentNoteIndex];
      note.content = textarea.value;
      note.ts = Date.now();
      saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
      noteState.dirty = false;
      panel.classList.remove('notepad-unsaved');
      if (showToastMsg) showToast('✅ Note saved.');
      updateStats();
    }

    function snapshotHistory() {
      const note = noteState.notes[noteState.currentNoteIndex];
      noteState.history.snaps.push({
        idx: noteState.currentNoteIndex,
        title: note.title,
        content: note.content,
        ts: Date.now()
      });
      if (noteState.history.snaps.length > 50) noteState.history.snaps.shift();
      saveHistory(noteState.charId, noteState.history);
    }
    const debouncedSnapshot = debounce(snapshotHistory, 60000);

    function updatePreview() {
      let text = textarea.value || '';
      const html = DOMPurify.sanitize(marked.parse(text));
      mdPreview.innerHTML = html;

      // Search highlight AFTER render
      if (noteState.searchQuery) {
        const q = noteState.searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const regex = new RegExp(q, 'gi');
        const walker = document.createTreeWalker(mdPreview, NodeFilter.SHOW_TEXT, null);
        const nodes = [];
        let node;
        while ((node = walker.nextNode())) nodes.push(node);

        nodes.forEach(n => {
          const txt = n.nodeValue;
          if (regex.test(txt)) {
            const span = document.createElement('span');
            span.innerHTML = txt.replace(regex, m => `<mark>${m}</mark>`);
            n.parentNode.replaceChild(span, n);
          }
        });
      }

      if (window.twemoji) window.twemoji.parse(mdPreview);

      noteState.dirty = textarea.value !== noteState.notes[noteState.currentNoteIndex].content;
      panel.classList.toggle('notepad-unsaved', noteState.dirty);
      updateStats();
      debouncedSnapshot();
    }

    function updateStats() {
      let wc = (textarea.value.trim().match(/\S+/g) || []).length;
      let charCount = textarea.value.length;
      let note = noteState.notes[noteState.currentNoteIndex];
      let date = note.ts ? new Date(note.ts).toLocaleString() : '';
      statBox.textContent = `Words: ${wc} | Chars: ${charCount} | Last saved: ${date}`;
      if (noteState.dirty) statBox.textContent += ' [unsaved]';
    }

    function confirmDelete() {
      if (noteState.notes.length <= 1) {
        if (confirm('This is the only note. Delete it?')) {
          noteState.notes = [{ title: 'Note 1', content: '', ts: Date.now() }];
          noteState.currentNoteIndex = 0;
          updateNoteSelector();
          loadSelectedNote();
          saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
          showToast('🗑 Note deleted. Created a new one.');
        }
      } else {
        if (confirm('Delete current note?')) {
          noteState.notes.splice(noteState.currentNoteIndex, 1);
          if (noteState.currentNoteIndex >= noteState.notes.length) {
            noteState.currentNoteIndex = noteState.notes.length - 1;
          }
          updateNoteSelector();
          loadSelectedNote();
          saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
          showToast('🗑 Note deleted.');
        }
      }
    }

    function createNewNote() {
      if (noteState.dirty && !confirm('You have unsaved changes! Discard and start new?')) return;
      const modal = createModal(`
        <h3>New Note</h3>
        <input type="text" id="np-new-title" placeholder="Note title">
      `, (m) => {
        const val = m.querySelector('#np-new-title').value.trim();
        const title = val || `Note ${noteState.notes.length + 1}`;
        noteState.notes.push({ title, content: '', ts: Date.now() });
        noteState.currentNoteIndex = noteState.notes.length - 1;
        updateNoteSelector();
        loadSelectedNote();
        saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
        showToast('🆕 New note created.');
      });
      modal.querySelector('#np-new-title').focus();
    }

    function duplicateNote() {
      const src = noteState.notes[noteState.currentNoteIndex];
      const clone = { title: src.title + ' (copy)', content: src.content, ts: Date.now() };
      noteState.notes.splice(noteState.currentNoteIndex + 1, 0, clone);
      noteState.currentNoteIndex++;
      updateNoteSelector();
      loadSelectedNote();
      saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
      showToast('📄 Note duplicated.');
    }

    function renameNote() {
      const current = noteState.notes[noteState.currentNoteIndex];
      const modal = createModal(`
        <h3>Rename Note</h3>
        <input type="text" id="np-rename-title" value="${current.title}">
      `, (m) => {
        const val = m.querySelector('#np-rename-title').value.trim();
        if (val) {
          current.title = val;
          updateNoteSelector();
          saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
          showToast('✏️ Note renamed.');
        }
      });
      modal.querySelector('#np-rename-title').focus();
    }

    function exportNotes() {
      let all = {};
      for (let k in localStorage) {
        if (k.startsWith('charai_supernotepad_') && !k.includes('_history_')) {
          all[k] = localStorage[k];
        }
      }
      downloadFile('characterai_notepad.json', JSON.stringify(all, null, 2), 'application/json');
      showToast('⭳ All notes exported as JSON.');
    }

    function exportCurrentMd() {
      const note = noteState.notes[noteState.currentNoteIndex];
      const fname = `charai_${noteState.charId}_${note.title}.md`.replace(/[^\w.-]+/g, '_');
      downloadFile(fname, note.content, 'text/markdown');
      showToast('⭳ Exported as .md');
    }

    function importNotes() {
      uploadFile((txt, filename) => {
        let isJson = false;
        let data = null;
        try { data = JSON.parse(txt); isJson = true; } catch {}
        if (isJson && data.notes) {
          noteState.notes = data.notes;
          noteState.currentNoteIndex = 0;
          updateNoteSelector();
          loadSelectedNote();
          saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
          showToast('⭱ Notes imported (JSON).');
        } else if (isJson && Object.keys(data).some(k => k.startsWith('charai_supernotepad_'))) {
          Object.keys(data).forEach(k => localStorage.setItem(k, data[k]));
          loadCurrentNote();
          showToast('⭱ Bulk import completed.');
        } else {
          const modal = createModal(`
            <h3>Import as New Note</h3>
            <p>File: ${filename}</p>
            <input type="text" id="np-import-title" placeholder="Imported Note Title" value="Imported Note">
          `, (m) => {
            const newTitle = m.querySelector('#np-import-title').value.trim() || 'Imported Note';
            noteState.notes.push({ title: newTitle, content: txt, ts: Date.now() });
            noteState.currentNoteIndex = noteState.notes.length - 1;
            updateNoteSelector();
            loadSelectedNote();
            saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
            showToast('⭱ Imported as new note.');
          });
          modal.querySelector('#np-import-title').focus();
        }
      });
    }

    function showHelp() {
      let msg = `Keyboard shortcuts:
Ctrl+Shift+N = Toggle Notepad
Ctrl+S = Save
Ctrl+Shift+M = Export current as .md
Ctrl+Shift+E = Export all (JSON)
Ctrl+Shift+I = Import
Ctrl+Shift+L = Cycle Theme
Ctrl+Shift+F = Toggle Floating
Ctrl+Shift+R = Rename Note
Ctrl+Shift+D = Duplicate Note
Ctrl+Shift+H = History
Ctrl+Shift+S = Shrink Panel
Ctrl+Shift+O = Side/Overlay Mode

- Autosave on idle
- Markdown + emoji supported
- Multiple notes per character
- Search highlight after render
- Shadow DOM isolation
- DOMPurify XSS sanitization
- History snapshots every ~60s while typing
      `;
      alert(msg);
    }

    function togglePanel(force) {
      const isOpen = force === true || (force === undefined && !panel.classList.contains('open'));
      const isOverlay = panel.classList.contains('overlay');
      panel.classList.toggle('open', isOpen);
      if (isOpen) loadCurrentNote();
      if (isOverlay && isOpen) {
        panel.style.left = '50%';
        panel.style.top = '50%';
      }
    }

    function showHistoryModal() {
      const hist = noteState.history.snaps.slice().reverse();
      if (!hist.length) {
        showToast('No history snapshots yet.');
        return;
      }
      const listHtml = hist.map((snap, idx) => {
        const date = new Date(snap.ts).toLocaleString();
        return `
          <div class="notepad-history-item" data-idx="${hist.length - 1 - idx}">
            <span>${snap.title} — <em>${date}</em></span>
            <button class="np-restore">Restore</button>
          </div>`;
      }).join('');
      const modal = createModal(`
        <h3>History (last ${hist.length})</h3>
        <div class="notepad-history-list">${listHtml}</div>
      `, null, null);

      modal.querySelectorAll('.np-restore').forEach(btn => {
        btn.onclick = (e) => {
          const parent = e.target.closest('.notepad-history-item');
          const idx = parseInt(parent.dataset.idx, 10);
          const snap = noteState.history.snaps[idx];
          if (!snap) return;
          const c = confirm('Restore this snapshot? This will overwrite current note content.');
          if (c) {
            if (snap.idx >= noteState.notes.length) {
              noteState.notes.push({ title: snap.title, content: snap.content, ts: Date.now() });
              noteState.currentNoteIndex = noteState.notes.length - 1;
            } else {
              noteState.currentNoteIndex = snap.idx;
              noteState.notes[snap.idx].title = snap.title;
              noteState.notes[snap.idx].content = snap.content;
              noteState.notes[snap.idx].ts = Date.now();
            }
            updateNoteSelector();
            loadSelectedNote();
            saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
            showToast('⏪ Snapshot restored.');
            modal.remove();
          }
        };
      });
    }

    // ------------------------ Event Bindings ------------------------
    noteSelector.onchange = () => {
      if (noteState.dirty) {
        if (!confirm('You have unsaved changes. Switch note anyway?')) {
          noteSelector.value = noteState.currentNoteIndex;
          return;
        }
      }
      noteState.currentNoteIndex = parseInt(noteSelector.value, 10);
      loadSelectedNote();
    };

    saveBtn.onclick = () => saveCurrentNote();
    newBtn.onclick = () => createNewNote();
    dupBtn.onclick = () => duplicateNote();
    renameBtn.onclick = () => renameNote();
    delBtn.onclick = () => confirmDelete();
    expBtn.onclick = () => exportNotes();
    impBtn.onclick = () => importNotes();
    histBtn.onclick = () => showHistoryModal();

    shrinkBtn.onclick = () => panel.classList.toggle('shrunk');
    floatBtn.onclick = () => panel.classList.toggle('floating');
    leftBtn.onclick = () => panel.classList.toggle('left');
    splitBtn.onclick = () => {
      noteState.split = !noteState.split;
      content.style.flexDirection = noteState.split ? 'row' : 'column';
      saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split });
    };
    helpBtn.onclick = () => showHelp();
    modeBtn.onclick = () => {
      const isSide = panel.classList.contains('side');
      panel.classList.toggle('side', !isSide);
      panel.classList.toggle('overlay', isSide);
    };
    fsBtn.onclick = () => {
      if (document.fullscreenElement) {
        document.exitFullscreen();
      } else {
        panel.requestFullscreen();
      }
    };
    tglBtn.onclick = () => togglePanel();

    themeSelector.value = localStorage.getItem('notepad_theme') || 'dark';
    setTheme(themeSelector.value);
    themeSelector.onchange = () => setTheme(themeSelector.value);

    const debouncedSave = debounce(() => saveCurrentNote(false), 4000);
    textarea.oninput = () => { updatePreview(); debouncedSave(); };
    textarea.onblur = () => { updatePreview(); saveCurrentNote(false); };

    searchInput.oninput = () => {
      noteState.searchQuery = searchInput.value.trim();
      updatePreview();
    };

    // Emoji picker
    emojiBtn.onclick = () => {
      if (emojiPicker) { emojiPicker.remove(); emojiPicker = null; return; }
      emojiPicker = document.createElement('div');
      emojiPicker.className = 'notepad-emoji-picker';
      "😀😄😎😜🤔🤯😭😡🔥🎉💡📌✨🚀👀🤖🤷🧠💀🙃🙄😂🥺👽💩🤡👑🏆🔒🦾🔑🧃".split('').forEach(e => {
        let eb = document.createElement('span');
        eb.className = 'notepad-emoji';
        eb.textContent = e;
        eb.onclick = () => {
          const start = textarea.selectionStart;
          const end = textarea.selectionEnd;
          textarea.setRangeText(e, start, end, 'end');
          textarea.focus();
          updatePreview();
        };
        emojiPicker.append(eb);
      });
      editorPane.append(emojiPicker);
      const closePicker = (ev) => {
        if (!emojiPicker.contains(ev.composedPath()[0]) && ev.target !== emojiBtn) {
          emojiPicker.remove(); emojiPicker = null;
          shadow.removeEventListener('click', closePicker, true);
        }
      };
      shadow.addEventListener('click', closePicker, true);
    };

    // Dragging overlay
    let isDragging = false;
    let startX, startY, startLeft, startTop;
    header.onpointerdown = (e) => {
      if (panel.classList.contains('overlay')) {
        isDragging = true;
        startX = e.clientX;
        startY = e.clientY;
        const rect = panel.getBoundingClientRect();
        startLeft = rect.left;
        startTop = rect.top;
        header.setPointerCapture(e.pointerId);
      }
    };
    header.onpointermove = (e) => {
      if (isDragging) {
        let dx = e.clientX - startX;
        let dy = e.clientY - startY;
        panel.style.left = (startLeft + dx) + 'px';
        panel.style.top = (startTop + dy) + 'px';
      }
    };
    header.onpointerup = (e) => {
      if (isDragging) {
        isDragging = false;
        header.releasePointerCapture(e.pointerId);
      }
    };

    // Keyboard Shortcuts
    window.addEventListener('keydown', (e) => {
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyN') { togglePanel(); e.preventDefault(); }
      if (e.ctrlKey && !e.shiftKey && e.code === 'KeyS') { saveCurrentNote(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyM') { exportCurrentMd(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyE') { exportNotes(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyI') { importNotes(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyL') {
        let t = themeSelector.selectedIndex + 1;
        if (t >= themeSelector.options.length) t = 0;
        themeSelector.selectedIndex = t;
        setTheme(themeSelector.value);
        showToast(`Theme: ${themeSelector.value}`);
        e.preventDefault();
      }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyF') { panel.classList.toggle('floating'); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyS') { panel.classList.toggle('shrunk'); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyO') { modeBtn.click(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyR') { renameBtn.click(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyD') { dupBtn.click(); e.preventDefault(); }
      if (e.ctrlKey && e.shiftKey && e.code === 'KeyH') { histBtn.click(); e.preventDefault(); }
    });

    window.addEventListener('beforeunload', e => {
      if (panel.classList.contains('open') && noteState.dirty) {
        e.preventDefault();
        e.returnValue = 'You have unsaved notepad changes!';
        return 'You have unsaved notepad changes!';
      }
    });

    // Watch for route changes
    let lastPath = location.pathname;
    setInterval(() => {
      if (location.pathname !== lastPath) {
        lastPath = location.pathname;
        let cid = getCharacterId() || 'global';
        if (cid !== noteState.charId) loadCurrentNote();
      }
    }, 700);

    // Cross-tab sync 
    window.addEventListener('storage', (e) => {
      if (e.key === noteKey(noteState.charId)) {
        const curNote = noteState.notes[noteState.currentNoteIndex];
        const wasDirty = noteState.dirty;
        const oldContent = textarea.value;
        loadCurrentNote();
        if (wasDirty && curNote && textarea.value !== oldContent) {
          showToast('⚠️ Notes changed in another tab.');
        }
      }
    });

    // Easter egg
    flagbar.onclick = () => window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank');

    // Initial load
    setTimeout(() => { loadCurrentNote(); }, 300);
  }
})();