/** * Nostr Chat Widget - Embeddable Version * * * * * * * * EMBED IT WITH: COLOR OPTIONS WITH: * * **/ (function() { 'use strict'; // Get configuration from script tag data attributes const currentScript = document.currentScript || document.querySelector('script[data-nostr-pubkey]'); const DEFAULT_RELAYS = [ 'wss://relay.btcforplebs.com', 'wss://nos.lol', 'wss://relay.nostr.band' ]; const csPubkey = currentScript?.getAttribute('data-nostr-pubkey') || ''; const brandName = currentScript?.getAttribute('data-brand-name') || 'Support Chat'; const primaryColor = currentScript?.getAttribute('data-color') || '#3b82f6'; const secondaryColor = currentScript?.getAttribute('data-color-secondary') || '#8b5cf6'; const relayUrls = currentScript?.getAttribute('data-relays')?.split(',') || DEFAULT_RELAYS; const powDifficulty = Math.max(8, parseInt(currentScript?.getAttribute('data-pow-difficulty') || '10', 10)); if (!csPubkey) { console.error('❌ Nostr Chat Widget: data-nostr-pubkey attribute is required'); return; } console.log('✅ Widget loaded with pubkey:', csPubkey); // Convert npub to hex if needed function npubToHex(npubOrHex) { if (npubOrHex.startsWith('npub')) { // Simple npub decode (you'd use nostr-tools in production) return npubOrHex; // Will be converted by nostr-tools } return npubOrHex; } // Inject custom styles const style = document.createElement('style'); style.textContent = ` .nostr-widget-safe-bottom { padding-bottom: env(safe-area-inset-bottom); } #nostr-chat-widget-root > div { pointer-events: auto !important; z-index: 99999 !important; } #nostr-chat-widget-root input:focus { outline: none; } @keyframes spin { to { transform: rotate(360deg); } } .animate-spin { animation: spin 1s linear infinite; } `; document.head.appendChild(style); // Create widget container const widgetRoot = document.createElement('div'); widgetRoot.id = 'nostr-chat-widget-root'; document.body.appendChild(widgetRoot); // Import map for nostr-tools const importMap = document.createElement('script'); importMap.type = 'importmap'; importMap.textContent = JSON.stringify({ imports: { 'nostr-tools/pool': 'https://esm.sh/nostr-tools@2.7.0/pool', 'nostr-tools/pure': 'https://esm.sh/nostr-tools@2.7.0/pure', 'nostr-tools/nip19': 'https://esm.sh/nostr-tools@2.7.0/nip19', 'nostr-tools/nip44': 'https://esm.sh/nostr-tools@2.7.0/nip44' } }); document.head.appendChild(importMap); // Main widget script const widgetScript = document.createElement('script'); widgetScript.type = 'module'; widgetScript.textContent = ` import { SimplePool } from 'nostr-tools/pool'; import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; import * as nip19 from 'nostr-tools/nip19'; import * as nip44 from 'nostr-tools/nip44'; // Create optimized Web Worker with inlined SHA-256 const workerCode = \` // Inlined optimized SHA-256 implementation function sha256(data) { const K = new Uint32Array([ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]); const bytes = new Uint8Array(data); const l = bytes.length * 8; const blocks = Math.ceil((l + 65) / 512); const msg = new Uint8Array(blocks * 64); msg.set(bytes); msg[bytes.length] = 0x80; const view = new DataView(msg.buffer); view.setUint32(msg.length - 4, l, false); let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a; let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19; const w = new Uint32Array(64); for (let i = 0; i < blocks; i++) { for (let j = 0; j < 16; j++) { w[j] = view.getUint32(i * 64 + j * 4, false); } for (let j = 16; j < 64; j++) { const s0 = ((w[j-15] >>> 7) | (w[j-15] << 25)) ^ ((w[j-15] >>> 18) | (w[j-15] << 14)) ^ (w[j-15] >>> 3); const s1 = ((w[j-2] >>> 17) | (w[j-2] << 15)) ^ ((w[j-2] >>> 19) | (w[j-2] << 13)) ^ (w[j-2] >>> 10); w[j] = (w[j-16] + s0 + w[j-7] + s1) | 0; } let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; for (let j = 0; j < 64; j++) { const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); const ch = (e & f) ^ (~e & g); const temp1 = (h + S1 + ch + K[j] + w[j]) | 0; const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); const maj = (a & b) ^ (a & c) ^ (b & c); const temp2 = (S0 + maj) | 0; h = g; g = f; f = e; e = (d + temp1) | 0; d = c; c = b; b = a; a = (temp1 + temp2) | 0; } h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0; } const hash = new Uint8Array(32); const hashView = new DataView(hash.buffer); hashView.setUint32(0, h0, false); hashView.setUint32(4, h1, false); hashView.setUint32(8, h2, false); hashView.setUint32(12, h3, false); hashView.setUint32(16, h4, false); hashView.setUint32(20, h5, false); hashView.setUint32(24, h6, false); hashView.setUint32(28, h7, false); return hash; } // NIP-13: Count leading zero bits in a hex string function countLeadingZeroes(hex) { let count = 0; for (let i = 0; i < hex.length; i++) { const nibble = parseInt(hex[i], 16); if (nibble === 0) { count += 4; } else { count += Math.clz32(nibble) - 28; break; } } return count; } function bytesToHex(bytes) { return Array.from(bytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } self.onmessage = function(e) { const { event, targetDifficulty } = e.data; const startTime = Date.now(); let nonce = 0; let bestDifficulty = 0; // Add nonce tag if not present if (!event.tags) event.tags = []; let nonceTagIndex = event.tags.findIndex(t => t[0] === 'nonce'); if (nonceTagIndex === -1) { event.tags.push(['nonce', '0', String(targetDifficulty)]); nonceTagIndex = event.tags.length - 1; } while (true) { event.tags[nonceTagIndex][1] = String(nonce); // Calculate event hash using inlined SHA-256 const eventData = [ 0, event.pubkey, event.created_at, event.kind, event.tags, event.content ]; const serialized = JSON.stringify(eventData); const encoder = new TextEncoder(); const data = encoder.encode(serialized); // Fast synchronous hash! const hash = sha256(data); event.id = bytesToHex(hash); const difficulty = countLeadingZeroes(event.id); if (difficulty > bestDifficulty) { bestDifficulty = difficulty; } // Report progress every 25k iterations if (nonce % 25000 === 0 && nonce > 0) { const elapsed = (Date.now() - startTime) / 1000; const hashRate = Math.round(nonce / elapsed); self.postMessage({ type: 'progress', nonce, difficulty: bestDifficulty, elapsed: elapsed.toFixed(1), hashRate }); } if (difficulty >= targetDifficulty) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); const hashRate = Math.round(nonce / parseFloat(elapsed)); self.postMessage({ type: 'complete', event, nonce, difficulty, elapsed, hashRate }); return; } nonce++; // Update created_at every 500k iterations if (nonce % 500000 === 0) { event.created_at = Math.floor(Date.now() / 1000); } } }; \`; const workerBlob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(workerBlob); // Compatible hex converters function hexToBytes(hex) { if (typeof hex !== 'string') { throw new Error('hex must be a string'); } if (hex.length % 2 !== 0) { throw new Error('hex string must have an even length'); } const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { const byte = parseInt(hex.slice(i, i + 2), 16); if (isNaN(byte)) { throw new Error('Invalid hex string'); } bytes[i / 2] = byte; } return bytes; } function bytesToHex(bytes) { return Array.from(bytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } // NIP-13: Count leading zero bits in a hex string function countLeadingZeroes(hex) { let count = 0; for (let i = 0; i < hex.length; i++) { const nibble = parseInt(hex[i], 16); if (nibble === 0) { count += 4; } else { count += Math.clz32(nibble) - 28; break; } } return count; } // Estimate mining time based on difficulty (optimized for instant messaging) function estimateTime(difficulty) { if (difficulty <= 8) return '<0.5s'; if (difficulty <= 10) return '~1s'; if (difficulty <= 12) return '1-2s'; if (difficulty <= 16) return '3-8s'; if (difficulty <= 20) return '15-45s'; if (difficulty <= 24) return '1-3min'; if (difficulty <= 28) return '3-8min'; return '>10min'; } // NIP-13: Mine proof-of-work using optimized Web Worker async function mineEvent(event, targetDifficulty) { return new Promise((resolve, reject) => { console.log(\`⛏️ Mining PoW with difficulty \${targetDifficulty} (optimized worker)...\`); const worker = new Worker(workerUrl); worker.onmessage = function(e) { if (e.data.type === 'progress') { console.log(\` Mining... nonce: \${e.data.nonce}, best: \${e.data.difficulty} bits, time: \${e.data.elapsed}s, rate: \${e.data.hashRate} H/s\`); } else if (e.data.type === 'complete') { console.log(\`✅ Mined! Difficulty: \${e.data.difficulty}, Nonce: \${e.data.nonce}, Time: \${e.data.elapsed}s, Avg: \${e.data.hashRate} H/s\`); worker.terminate(); resolve(e.data.event); } }; worker.onerror = function(error) { console.error('Mining error:', error); worker.terminate(); reject(error); }; worker.postMessage({ event, targetDifficulty }); }); } const CONFIG = { relays: ${JSON.stringify(relayUrls)}, csPubkey: '${npubToHex(csPubkey)}'.trim().toLowerCase(), brandName: '${brandName}', primaryColor: '${primaryColor}', secondaryColor: '${secondaryColor}', powDifficulty: ${powDifficulty} }; // Convert npub to hex if needed if (CONFIG.csPubkey.startsWith('npub')) { try { const decoded = nip19.decode(CONFIG.csPubkey); CONFIG.csPubkey = decoded.data.toLowerCase(); console.log('✅ Decoded npub to hex:', CONFIG.csPubkey.substring(0, 8) + '...'); } catch (error) { console.error('❌ Failed to decode npub:', error); alert('Invalid Nostr pubkey provided. Please check data-nostr-pubkey attribute.'); throw error; } } // Validate hex pubkey format and check if it's valid if (!CONFIG.csPubkey || CONFIG.csPubkey.length !== 64) { console.error('❌ Invalid pubkey length:', CONFIG.csPubkey?.length, 'Expected: 64'); console.error('❌ Pubkey value:', CONFIG.csPubkey); alert('Invalid Nostr pubkey. Expected 64-character hex string or npub.'); throw new Error('Invalid pubkey'); } if (!/^[0-9a-f]{64}$/.test(CONFIG.csPubkey)) { console.error('❌ Pubkey contains invalid characters'); alert('Invalid Nostr pubkey. Must be 64 hex characters.'); throw new Error('Invalid pubkey format'); } // Test if pubkey is valid by trying to use it (with CORRECT hex format) console.log('✅ Testing pubkey validity...'); try { const testPriv = generateSecretKey(); const testPrivHex = bytesToHex(testPriv); nip44.v2.utils.getConversationKey(testPrivHex, CONFIG.csPubkey); console.log('✅ Pubkey is valid!'); } catch (e) { console.error('❌ Pubkey failed validation test:', e.message); alert(\`The provided pubkey appears to be invalid or not a valid secp256k1 public key. Error: \${e.message}\`); throw new Error('Invalid pubkey - failed cryptographic validation'); } console.log('✅ Widget initialized with pubkey:', CONFIG.csPubkey.substring(0, 8) + '...'); console.log('✅ Pubkey length:', CONFIG.csPubkey.length); console.log('✅ PoW difficulty:', CONFIG.powDifficulty + ' bits - Optimized for instant messaging'); console.log('✅ Relays:', CONFIG.relays.join(', ')); let state = { isOpen: false, messages: [], inputMessage: '', myPrivKey: null, myPubKey: null, pool: null, connected: false, sessionId: null, isMining: false }; // NIP-17 Helper Functions function randomNow() { return Math.floor(Date.now() / 1000) - Math.floor(Math.random() * (2 * 24 * 60 * 60)); } function randomDelay() { return Math.floor(Math.random() * 2 * 24 * 60 * 60); } async function createSeal(rumor, recipientPubkey, senderPrivkey) { if (!recipientPubkey || !senderPrivkey) { throw new Error('Missing keys for seal creation'); } console.log('🔒 Creating seal'); const rumorEvent = { kind: 14, created_at: rumor.created_at || randomNow(), tags: rumor.tags || [], content: rumor.content, pubkey: bytesToHex(getPublicKey(senderPrivkey)) }; try { // CRITICAL: getConversationKey expects BOTH args as HEX STRINGS const senderPrivkeyHex = bytesToHex(senderPrivkey); const recipientPubkeyHex = recipientPubkey; // already hex console.log(' - Sender privkey (hex):', senderPrivkeyHex.substring(0, 8) + '...'); console.log(' - Recipient pubkey (hex):', recipientPubkeyHex.substring(0, 8) + '...'); const conversationKey = nip44.v2.utils.getConversationKey( senderPrivkeyHex, recipientPubkeyHex ); console.log(' - ✅ Got conversation key'); const encrypted = nip44.v2.encrypt( JSON.stringify(rumorEvent), conversationKey ); const seal = { kind: 13, created_at: randomNow(), tags: [], content: encrypted, pubkey: bytesToHex(getPublicKey(senderPrivkey)) }; return finalizeEvent(seal, senderPrivkey); } catch (error) { console.error('❌ Error in createSeal:', error); throw error; } } async function createGiftWrap(seal, recipientPubkey) { if (!recipientPubkey) { throw new Error('Missing recipient pubkey for gift wrap'); } console.log('🎁 Creating gift wrap for:', recipientPubkey.substring(0, 8) + '...'); const randomKey = generateSecretKey(); const randomKeyHex = bytesToHex(randomKey); // CRITICAL: getConversationKey expects BOTH args as HEX STRINGS const conversationKey = nip44.v2.utils.getConversationKey( randomKeyHex, recipientPubkey ); const encrypted = nip44.v2.encrypt( JSON.stringify(seal), conversationKey ); let giftWrap = { kind: 1059, created_at: randomNow(), tags: [['p', recipientPubkey]], content: encrypted, pubkey: bytesToHex(getPublicKey(randomKey)) }; // Mine proof-of-work (ALWAYS REQUIRED) giftWrap = await mineEvent(giftWrap, CONFIG.powDifficulty); return finalizeEvent(giftWrap, randomKey); } async function unwrapGift(giftWrap, recipientPrivkey) { try { console.log('🎁 Unwrapping gift from:', giftWrap.pubkey.substring(0, 8) + '...'); // CRITICAL: getConversationKey expects BOTH args as HEX STRINGS const recipientPrivkeyHex = bytesToHex(recipientPrivkey); const senderPubkeyHex = giftWrap.pubkey; const conversationKey = nip44.v2.utils.getConversationKey( recipientPrivkeyHex, senderPubkeyHex ); const decryptedSeal = nip44.v2.decrypt( giftWrap.content, conversationKey ); const seal = JSON.parse(decryptedSeal); const rumorConversationKey = nip44.v2.utils.getConversationKey( recipientPrivkeyHex, seal.pubkey ); const decryptedRumor = nip44.v2.decrypt( seal.content, rumorConversationKey ); const rumor = JSON.parse(decryptedRumor); return { content: rumor.content, sender: rumor.pubkey, created_at: rumor.created_at, tags: rumor.tags }; } catch (error) { console.error('Error unwrapping gift:', error); return null; } } // Session Management function getSessionKey() { const stored = localStorage.getItem('nostr_chat_session'); if (stored) { try { const { privKey, pubKey, sessionId } = JSON.parse(stored); console.log('📂 Loaded session from storage'); const privKeyArray = new Uint8Array(privKey); console.log(' - Loaded privkey length:', privKeyArray.length); console.log(' - Loaded pubkey:', pubKey.substring(0, 8) + '...'); return { privKey: privKeyArray, pubKey, sessionId }; } catch (e) { console.error('❌ Failed to load session:', e); localStorage.removeItem('nostr_chat_session'); } } console.log('🆕 Creating new session'); const privKey = generateSecretKey(); const pubKey = bytesToHex(getPublicKey(privKey)); const sessionId = \`session_\${Date.now()}_\${Math.random().toString(36).substr(2, 9)}\`; console.log(' - New privkey length:', privKey.length); console.log(' - New pubkey:', pubKey.substring(0, 8) + '...'); console.log(' - New pubkey length:', pubKey.length); localStorage.setItem('nostr_chat_session', JSON.stringify({ privKey: Array.from(privKey), pubKey, sessionId })); return { privKey, pubKey, sessionId }; } // Initialize async function init() { try { console.log('🚀 Initializing NIP-17 Chat Widget...'); const session = getSessionKey(); state.myPrivKey = session.privKey; // Already Uint8Array from getSessionKey state.myPubKey = session.pubKey; state.sessionId = session.sessionId; console.log(\`👤 Session: \${nip19.npubEncode(state.myPubKey).substring(0, 16)}...\`); // Create pool state.pool = new SimplePool(); state.connected = true; console.log('✅ Relay pool initialized'); loadPreviousMessages(); subscribeToReplies(); addMessage('system', \`Ready! Messages mine \${CONFIG.powDifficulty}-bit PoW (\${estimateTime(CONFIG.powDifficulty)})\`); } catch (error) { console.error('❌ Initialization error:', error); addMessage('system', '⚠️ Failed to initialize: ' + error.message); throw error; } } function addMessage(sender, text, fullMessage = null) { const message = fullMessage || { id: \`msg_\${Date.now()}_\${Math.random()}\`, text, sender, timestamp: new Date().toISOString() }; state.messages.push(message); saveMessages(); render(); } function formatTime(timestamp) { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } function updateConnectionStatus() { state.connected = state.pool !== null; render(); } function subscribeToReplies() { const filters = [{ kinds: [1059], '#p': [state.myPubKey], since: Math.floor(Date.now() / 1000) - 86400 }]; console.log('📡 Subscribing to NIP-17 messages...'); try { const sub = state.pool.subscribeMany( CONFIG.relays, filters, { onevent(event) { handleIncomingMessage(event).catch(err => { console.error('Error handling incoming message:', err); }); }, oneose() { console.log(\`✓ Subscription active\`); }, onclose(reason) { console.log('Subscription closed:', reason); } } ); } catch (error) { console.error('❌ Failed to subscribe:', error); } } function loadPreviousMessages() { const stored = localStorage.getItem(\`nostr_chat_messages_\${state.sessionId}\`); if (stored) { try { const messages = JSON.parse(stored); messages.forEach(msg => state.messages.push(msg)); render(); } catch (e) {} } } function saveMessages() { localStorage.setItem(\`nostr_chat_messages_\${state.sessionId}\`, JSON.stringify(state.messages)); } async function handleIncomingMessage(event) { try { if (state.messages.find(m => m.id === event.id)) { return; } console.log('📨 Received gift wrap'); const unwrapped = await unwrapGift(event, state.myPrivKey); if (!unwrapped) { console.error('Failed to unwrap message'); return; } if (unwrapped.sender !== CONFIG.csPubkey) { console.log('Ignoring message from unknown sender'); return; } const message = { id: event.id, text: unwrapped.content, sender: 'cs', timestamp: new Date(unwrapped.created_at * 1000).toISOString() }; addMessage('cs', unwrapped.content, message); if (!document.hasFocus()) { const originalTitle = document.title; document.title = '💬 New message!'; setTimeout(() => { document.title = originalTitle; }, 3000); } } catch (error) { console.error('Error processing message:', error); } } async function sendMessage() { if (!state.inputMessage.trim() || state.isMining) return; const messageText = state.inputMessage; state.inputMessage = ''; state.isMining = true; render(); try { console.log('🔐 Creating NIP-17 message...'); console.log('- My pubkey:', state.myPubKey?.substring(0, 8) + '...'); console.log('- My pubkey length:', state.myPubKey?.length); console.log('- My privkey type:', state.myPrivKey?.constructor.name); console.log('- My privkey length:', state.myPrivKey?.length); console.log('- Recipient:', CONFIG.csPubkey?.substring(0, 8) + '...'); console.log('- Recipient length:', CONFIG.csPubkey?.length); if (!state.myPrivKey || !CONFIG.csPubkey) { throw new Error('Missing encryption keys'); } // Ensure privkey is Uint8Array if (!(state.myPrivKey instanceof Uint8Array)) { console.error('❌ Private key is not Uint8Array:', typeof state.myPrivKey); throw new Error('Invalid private key format'); } const rumor = { content: messageText, created_at: Math.floor(Date.now() / 1000), tags: [['p', CONFIG.csPubkey]] }; addMessage('system', '⛏️ Mining...'); const seal = await createSeal(rumor, CONFIG.csPubkey, state.myPrivKey); const giftWrap = await createGiftWrap(seal, CONFIG.csPubkey); console.log('📤 Publishing to relays...'); // SimplePool.publish returns an array of promises try { const publishPromises = state.pool.publish(CONFIG.relays, giftWrap); const results = await Promise.allSettled(publishPromises); const published = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected'); if (failed.length > 0) { console.log(\`⚠️ Failed to publish to \${failed.length} relays\`); failed.forEach((result, i) => { const error = result.reason?.message || result.reason || 'Unknown error'; console.error(\` - Relay \${CONFIG.relays[i]}: \${error}\`); // Detect PoW requirement if (error.includes('pow:') || error.includes('bits needed')) { console.warn(\` ⚠️ This relay requires proof-of-work\`); } }); } if (published === 0) { addMessage('system', '⚠️ Failed to send - no relay connections'); return; } console.log(\`✅ Message sent to \${published}/\${CONFIG.relays.length} relays\`); } catch (publishError) { console.error('❌ Publish error:', publishError); addMessage('system', '⚠️ Failed to publish message: ' + publishError.message); return; } const message = { id: giftWrap.id, text: messageText, sender: 'user', timestamp: new Date().toISOString() }; addMessage('user', messageText, message); render(); } catch (error) { console.error('❌ Error sending message:', error); console.error('Stack:', error.stack); addMessage('system', '⚠️ Failed to send message: ' + error.message); } finally { state.isMining = false; render(); } } function render() { const root = document.getElementById('nostr-chat-widget-root'); if (!root) return; root.innerHTML = \`
h?l[c][f]=s+1:n.charAt(c-1)===i.charAt(f-1)?l[c][f]=l[c-1][f-1]:l[c][f]=Math.min(l[c-1][f-1]+1,Math.min(l[c][f-1]+1,l[c-1][f]+1)),l[c][f]{u();function Xg(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Zg(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Zg(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Xg(r[i],e)+t;return t}return Xg(r,e)}Jg.exports=Zg});var ry=x((lq,ty)=>{u();var Cs="-".charCodeAt(0),_s="+".charCodeAt(0),Fl=".".charCodeAt(0),j2="e".charCodeAt(0),z2="E".charCodeAt(0);function U2(r){var e=r.charCodeAt(0),t;if(e===_s||e===Cs){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Fl&&i>=48&&i<=57}return e===Fl?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}ty.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!U2(r))return!1;for(i=r.charCodeAt(e),(i===_s||i===Cs)&&e++;e{u();function Gy(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Qy(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Qy(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gy(r[i],e)+t;return t}return Gy(r,e)}Yy.exports=Qy});var Zy=x((o$,Xy)=>{u();var $s="-".charCodeAt(0),Ls="+".charCodeAt(0),su=".".charCodeAt(0),vO="e".charCodeAt(0),xO="E".charCodeAt(0);function kO(r){var e=r.charCodeAt(0),t;if(e===Ls||e===$s){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===su&&i>=48&&i<=57}return e===su?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Xy.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!kO(r))return!1;for(i=r.charCodeAt(e),(i===Ls||i===$s)&&e++;e