/* =================================================== JAESWIFT.XYZ — JAE-AI Agentic Chat Terminal Tiered model routing + tool calls + wallet auth =================================================== */ (function () { 'use strict'; const chatMessages = document.getElementById('chatMessages'); const chatInput = document.getElementById('chatInput'); const chatSend = document.getElementById('chatSend'); const chatStatus = document.getElementById('chatStatus'); const chatHeader = document.querySelector('.chat-terminal .panel-header'); if (!chatMessages || !chatInput || !chatSend) return; const mem = window.chatMemory || null; let history = []; let isWaiting = false; let currentTier = 'anonymous'; let currentAddress = null; // ─── Utility ────────────────────────────────────── function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; }); } // ─── Minimal safe Markdown → HTML (assistant messages only) ── function md(text) { if (!text) return ''; let html = String(text).replace(/&/g, '&').replace(//g, '>'); // Fenced code blocks ```...``` html = html.replace(/```([\s\S]*?)```/g, function (_m, code) { return '
' + code + '
'; }); // Inline code html = html.replace(/`([^`\n]+)`/g, '$1'); // Bold **...** html = html.replace(/\*\*([^*\n]+)\*\*/g, '$1'); // Italic *...* (avoid double-asterisk leftovers) html = html.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1$2'); // Links [text](url) html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); // Headings html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Bullet list lines: "- item" / "* item" →
  • html = html.replace(/^(?:-|\*) (.+)$/gm, '
  • $1
  • '); html = html.replace(/(
  • [\s\S]*?<\/li>)(?!\s*
  • )/g, ''); // Line breaks (avoid inside
    )
            html = html.replace(/(
    [\s\S]*?<\/pre>)|\n/g, function (m, pre) {
                return pre ? pre : '
    '; }); return html; } // ─── Render a message bubble ────────────────────── function addMessage(role, text) { const welcome = chatMessages.querySelector('.chat-welcome'); if (welcome) welcome.remove(); const msg = document.createElement('div'); msg.className = `chat-msg chat-msg-${role}`; const label = document.createElement('span'); label.className = 'chat-msg-label'; label.textContent = role === 'user' ? 'YOU' : 'JAE-AI'; const body = document.createElement('div'); body.className = 'chat-msg-body'; if (role === 'assistant') { body.innerHTML = md(text); } else { body.textContent = text; } msg.appendChild(label); msg.appendChild(body); chatMessages.appendChild(msg); chatMessages.scrollTop = chatMessages.scrollHeight; return body; } function addToolCallCard(call) { const wrap = document.createElement('div'); wrap.className = 'agent-tool-call'; const argsStr = escapeHtml(JSON.stringify(call.args || {}, null, 0)); const result = call.result || {}; const errored = !!result.error; const statusIcon = errored ? '❌' : '✅'; let summary = errored ? (result.error || 'error') : 'ok'; if (!errored) { if (Array.isArray(result.results)) summary = `${result.results.length} result${result.results.length === 1 ? '' : 's'}`; else if (Array.isArray(result.entries)) summary = `${result.entries.length} entries`; else if (typeof result.price_usd === 'number') summary = `$${result.price_usd.toFixed(2)} (${result.change_24h_pct > 0 ? '+' : ''}${(result.change_24h_pct || 0).toFixed(2)}%)`; else if (result.effect) summary = `effect: ${result.effect}`; else if (result.balance_sol !== undefined) summary = `${result.balance_sol} SOL`; } const resultStr = escapeHtml(JSON.stringify(result, null, 2).slice(0, 1400)); wrap.innerHTML = `
    🔧 ${escapeHtml(call.name)} (${argsStr})
    ├─ ${statusIcon} ${escapeHtml(summary)}
    └─ (click to expand)
    ${resultStr}
    `; chatMessages.appendChild(wrap); chatMessages.scrollTop = chatMessages.scrollHeight; } // ─── Typing indicator ───────────────────────────── function showTyping() { const indicator = document.createElement('div'); indicator.className = 'chat-msg chat-msg-assistant chat-typing-indicator'; indicator.id = 'chatTyping'; indicator.innerHTML = ` JAE-AI
    `; chatMessages.appendChild(indicator); chatMessages.scrollTop = chatMessages.scrollHeight; } function hideTyping() { const el = document.getElementById('chatTyping'); if (el) el.remove(); } // ─── Typewriter effect ──────────────────────────── function typewriterEffect(element, text, speed) { speed = speed || 10; element.innerHTML = ''; // Bypass animation for long replies — render full markdown immediately if (!text || text.length > 600) { element.innerHTML = md(text || ''); chatMessages.scrollTop = chatMessages.scrollHeight; return Promise.resolve(); } let i = 0; return new Promise(function (resolve) { function tick() { if (i < text.length) { i++; element.innerHTML = md(text.slice(0, i)); chatMessages.scrollTop = chatMessages.scrollHeight; setTimeout(tick, speed); } else { // Dispatch event so voice-mode / others can hook try { document.dispatchEvent(new CustomEvent('jae-agent-reply', { detail: { text: text } })); } catch (e) {} resolve(); } } tick(); }); } // ─── History restore ────────────────────────────── function restoreHistory() { if (!mem) return false; const saved = mem.getHistory(); if (!saved || saved.length === 0) return false; const welcome = chatMessages.querySelector('.chat-welcome'); if (welcome) welcome.remove(); saved.forEach(function (m) { const role = m.role === 'user' ? 'user' : 'assistant'; const label = document.createElement('span'); label.className = 'chat-msg-label'; label.textContent = role === 'user' ? 'YOU' : 'JAE-AI'; const body = document.createElement('div'); body.className = 'chat-msg-body'; if (role === 'assistant') { body.innerHTML = md(m.content || ''); } else { body.textContent = m.content || ''; } const el = document.createElement('div'); el.className = `chat-msg chat-msg-${role}`; el.appendChild(label); el.appendChild(body); chatMessages.appendChild(el); }); const divider = document.createElement('div'); divider.className = 'chat-divider'; divider.innerHTML = '— RESTORED FROM MEMORY —'; chatMessages.appendChild(divider); chatMessages.scrollTop = chatMessages.scrollHeight; history = saved.slice(); return true; } // ─── Tier badge ─────────────────────────────────── const TIER_DISPLAY = { anonymous: { label: 'ANONYMOUS', icon: '👁' }, operator: { label: 'OPERATOR', icon: '🎖️' }, elite: { label: 'ELITE', icon: '⭐' }, admin: { label: 'ADMIN', icon: '🛠️' }, }; function renderTierBadge() { if (!chatHeader) return; let badge = document.getElementById('chatTierBadge'); if (!badge) { badge = document.createElement('span'); badge.id = 'chatTierBadge'; badge.className = 'tier-badge'; chatHeader.appendChild(badge); } const info = TIER_DISPLAY[currentTier] || TIER_DISPLAY.anonymous; badge.className = 'tier-badge tier-' + currentTier; badge.textContent = info.icon + ' ' + info.label; badge.title = currentAddress ? (currentAddress.slice(0, 4) + '…' + currentAddress.slice(-4)) : 'Not authenticated'; } // ─── Wallet auth handshake ──────────────────────── async function refreshWhoAmI() { try { const r = await fetch('/api/auth/whoami', { credentials: 'same-origin' }); if (!r.ok) return; const j = await r.json(); currentTier = j.tier || 'anonymous'; currentAddress = j.address || null; renderTierBadge(); } catch (e) { /* silent */ } } async function walletSignIn(address, provider) { if (!address || !provider) return; try { // 1. Get nonce const nr = await fetch('/api/auth/nonce', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address }), }); if (!nr.ok) { console.warn('nonce fetch failed'); return; } const { message, nonce } = await nr.json(); // 2. Sign message const encoded = new TextEncoder().encode(message); let sigBytes; if (typeof provider.signMessage === 'function') { const res = await provider.signMessage(encoded, 'utf8'); sigBytes = res && (res.signature || res); } else if (provider.features && provider.features['solana:signMessage']) { const feat = provider.features['solana:signMessage']; const res = await feat.signMessage({ message: encoded }); sigBytes = Array.isArray(res) ? res[0].signature : res.signature; } else { console.warn('wallet does not support signMessage'); return; } if (!sigBytes) return; // sigBytes -> base58 const sigB58 = bs58encode(sigBytes); // 3. Verify const vr = await fetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ address, signature: sigB58, nonce }), }); if (!vr.ok) { const err = await vr.json().catch(() => ({})); console.warn('verify failed:', err); return; } const j = await vr.json(); currentTier = j.tier || 'operator'; currentAddress = j.address || address; renderTierBadge(); const info = TIER_DISPLAY[currentTier]; addMessage('assistant', `🔓 Wallet verified. Tier: ${info.icon} ${info.label}. Model: ${j.model || 'default'}.`); } catch (e) { console.warn('walletSignIn error', e); } } // Minimal bs58 encoder (no external deps) const BS58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; function bs58encode(bytes) { if (!bytes) return ''; if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes); const digits = [0]; for (let i = 0; i < bytes.length; i++) { let carry = bytes[i]; for (let j = 0; j < digits.length; j++) { carry += digits[j] << 8; digits[j] = carry % 58; carry = (carry / 58) | 0; } while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; } } let str = ''; for (let k = 0; k < bytes.length && bytes[k] === 0; k++) str += '1'; for (let q = digits.length - 1; q >= 0; q--) str += BS58_ALPHABET[digits[q]]; return str; } // Listen to wallet-connect events window.addEventListener('wallet-connected', function (e) { const detail = e.detail || {}; const addr = detail.address || (window.solWallet && window.solWallet.address); const provider = detail.provider || (window.solWallet && window.solWallet.provider); if (addr && provider) walletSignIn(addr, provider); }); window.addEventListener('wallet-disconnected', async function () { try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); } catch (e) {} currentTier = 'anonymous'; currentAddress = null; renderTierBadge(); }); // ─── Send message to agent ──────────────────────── async function sendMessage() { const text = chatInput.value.trim(); if (!text || isWaiting) return; // CLI interception (slash commands, or nested CLI mode) const inCliMode = window.__jaeCLI && typeof window.__jaeCLI.isInMode === 'function' && window.__jaeCLI.isInMode(); if ((text.startsWith('/') || inCliMode) && window.__jaeCLI && typeof window.__jaeCLI.handle === 'function') { isWaiting = true; chatInput.value = ''; const userBubble = addMessage('user', text); const userMsg = userBubble && userBubble.parentElement; if (userMsg) userMsg.classList.add('chat-msg-user-cli'); try { const res = await window.__jaeCLI.handle(text); if (res && res.handled) { if (res.output) { const body = addMessage('assistant', ''); const msgEl = body && body.parentElement; if (msgEl) msgEl.classList.add('chat-msg-cli'); body.innerHTML = md(res.output); chatMessages.scrollTop = chatMessages.scrollHeight; } } } catch (e) { const body = addMessage('assistant', 'cli error: ' + e.message); const msgEl = body && body.parentElement; if (msgEl) msgEl.classList.add('chat-msg-cli'); } isWaiting = false; chatInput.focus(); return; } isWaiting = true; chatInput.value = ''; chatStatus.textContent = '● PROCESSING'; chatStatus.classList.remove('status-green'); chatStatus.classList.add('status-amber'); addMessage('user', text); history.push({ role: 'user', content: text }); if (mem) mem.appendHistory({ role: 'user', content: text }); showTyping(); // Build conversation window (last 20 exchanges) const convo = history.slice(-20).map(function (m) { return { role: m.role, content: m.content }; }); const memoryBlock = mem ? mem.getContextBlock(text) : ''; try { const resp = await fetch('/api/agent/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ messages: convo, memory_context: memoryBlock, }), }); hideTyping(); if (!resp.ok) { const err = await resp.json().catch(function () { return {}; }); addMessage('assistant', 'ERROR: ' + (err.error || ('HTTP ' + resp.status))); chatStatus.textContent = '● ERROR'; chatStatus.classList.remove('status-amber'); chatStatus.classList.add('status-red'); isWaiting = false; return; } const data = await resp.json(); // Update tier from response if (data.tier) { currentTier = data.tier; renderTierBadge(); } // Render tool calls if (Array.isArray(data.tool_calls)) { data.tool_calls.forEach(addToolCallCard); } const reply = data.content || '(no reply)'; history.push({ role: 'assistant', content: reply }); if (mem) mem.appendHistory({ role: 'assistant', content: reply }); if (history.length > 40) history = history.slice(-30); const bodyEl = addMessage('assistant', ''); bodyEl.parentElement.classList.add('chat-msg-agent'); bodyEl.parentElement.setAttribute('data-model', data.model_used || ''); await typewriterEffect(bodyEl, reply, 8); // Execute frontend actions (effects etc.) if (Array.isArray(data.frontend_actions)) { data.frontend_actions.forEach(function (act) { if (act.action === 'trigger_effect' && window.__jaeEffects && typeof window.__jaeEffects.toggle === 'function') { try { window.__jaeEffects.toggle(act.effect); } catch (e) { console.warn('effect toggle failed', e); } } }); } if (mem) { mem.incrementUserMsgAndMaybeExtract().catch(function () { /* silent */ }); } } catch (e) { hideTyping(); addMessage('assistant', 'ERROR: Network failure — ' + e.message); chatStatus.textContent = '● OFFLINE'; chatStatus.classList.remove('status-amber'); chatStatus.classList.add('status-red'); } chatStatus.textContent = '● ONLINE'; chatStatus.classList.remove('status-amber', 'status-red'); chatStatus.classList.add('status-green'); isWaiting = false; chatInput.focus(); } // ─── Memory modal ───────────────────────────────── function formatDate(ts) { try { return new Date(ts).toLocaleString('en-GB'); } catch (e) { return ''; } } function renderMemoryModal() { if (!mem) return; const existing = document.getElementById('memModal'); if (existing) { existing.remove(); return; } const modal = document.createElement('div'); modal.id = 'memModal'; modal.className = 'mem-modal'; const all = mem.getAll(); const grouped = {}; all.forEach(function (m) { const c = m.category || 'other'; if (!grouped[c]) grouped[c] = []; grouped[c].push(m); }); const categories = ['identity', 'preference', 'project', 'skill', 'goal', 'relationship', 'other']; const order = categories.filter(function (c) { return grouped[c]; }) .concat(Object.keys(grouped).filter(function (c) { return categories.indexOf(c) === -1; })); let bodyHTML = ''; if (all.length === 0) { bodyHTML = '
    NO MEMORIES STORED // CHAT TO BUILD PROFILE
    '; } else { order.forEach(function (cat) { bodyHTML += '
    ' + cat.toUpperCase() + ' (' + grouped[cat].length + ')
    '; grouped[cat].sort(function (a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }).forEach(function (m) { bodyHTML += '
    ' + '
    ' + escapeHtml(m.text) + '
    ' + '
    imp: ' + (m.importance || 0).toFixed(2) + ' // ' + formatDate(m.timestamp) + '
    ' + '' + '
    '; }); bodyHTML += '
    '; }); } modal.innerHTML = `
    🧠 JAE-AI MEMORY VAULT
    STORED LOCALLY // ${all.length} MEMORIES // NEVER LEAVES DEVICE
    ${bodyHTML}
    `; document.body.appendChild(modal); document.getElementById('memClose').addEventListener('click', function () { modal.remove(); }); modal.addEventListener('click', function (e) { if (e.target === modal) modal.remove(); }); modal.querySelectorAll('.mem-del').forEach(function (btn) { btn.addEventListener('click', function () { const id = btn.getAttribute('data-id'); mem.remove(id); renderMemoryModal(); renderMemoryModal(); }); }); document.getElementById('memExport').addEventListener('click', function () { const blob = new Blob([mem.exportJSON()], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'jae-ai-memory-' + Date.now() + '.json'; a.click(); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); }); document.getElementById('memExtract').addEventListener('click', function () { const btn = document.getElementById('memExtract'); btn.disabled = true; btn.textContent = '⌛ EXTRACTING...'; mem.extractFromRecentChat().then(function () { renderMemoryModal(); renderMemoryModal(); }).catch(function () { btn.textContent = '✖ EXTRACT FAILED'; }); }); document.getElementById('memClearHist').addEventListener('click', function () { if (confirm('Clear chat history? Memories will be kept.')) { mem.clearHistory(); alert('Chat history cleared. Reload the page.'); } }); document.getElementById('memClear').addEventListener('click', function () { if (confirm('PERMANENTLY DELETE ALL MEMORIES? This cannot be undone.')) { mem.clear(); renderMemoryModal(); renderMemoryModal(); } }); } // ─── Header UI injection ────────────────────────── function injectMemoryUI() { if (!chatHeader) return; if (document.getElementById('memBtn')) return; const btn = document.createElement('button'); btn.id = 'memBtn'; btn.className = 'chat-mem-btn'; btn.title = 'View / manage local memories'; btn.innerHTML = '🧠 0'; btn.addEventListener('click', renderMemoryModal); const info = document.createElement('a'); info.href = '#'; info.className = 'chat-mem-info'; info.title = 'All memories stored locally in your browser. Nothing leaves your device except current message context.'; info.textContent = 'ℹ'; info.addEventListener('click', function (e) { e.preventDefault(); alert('PRIVACY NOTICE\n\nAll JAE-AI memories are stored locally in your browser (localStorage). They never leave your device.\n\nPer message, only the current text plus a small set of relevant memories is sent as context to Venice AI to provide continuity.\n\nYou can view, export, or clear all memories via the 🧠 button.'); }); chatHeader.appendChild(info); chatHeader.appendChild(btn); updateMemCount(); } function updateMemCount() { const el = document.getElementById('memBtnCount'); if (el && mem) el.textContent = mem.getAll().length; } // ─── Event listeners ────────────────────────────── chatSend.addEventListener('click', sendMessage); chatInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // ─── Init ───────────────────────────────────────── injectMemoryUI(); renderTierBadge(); refreshWhoAmI(); setInterval(updateMemCount, 5000); const restored = restoreHistory(); if (!restored) { setTimeout(function () { showTyping(); setTimeout(function () { hideTyping(); const greeting = 'Welcome to JAESWIFT.XYZ — I\'m JAE-AI. I can now search the site, fetch crypto prices, scan Solana wallets, look up .sol domains, pull SITREPs, and trigger visual effects. Connect your wallet for the Elite tier (1+ SOL) with Kimi-K2-Thinking. Ask me anything.'; history.push({ role: 'assistant', content: greeting }); if (mem) mem.appendHistory({ role: 'assistant', content: greeting }); const bodyEl = addMessage('assistant', ''); typewriterEffect(bodyEl, greeting, 10); }, 1500); }, 2000); } // Expose minimal API window.__jaeChat = { refreshWhoAmI: refreshWhoAmI, getTier: function () { return currentTier; }, getAddress: function () { return currentAddress; }, }; })();