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