/* ===================================================
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];
});
}
// ─── 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';
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 = `
├─ ${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;
// If reply is too long, bypass animation
if (!text || text.length > 600) {
element.textContent = text || '';
chatMessages.scrollTop = chatMessages.scrollHeight;
return Promise.resolve();
}
let i = 0;
element.textContent = '';
return new Promise(function (resolve) {
function tick() {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
chatMessages.scrollTop = chatMessages.scrollHeight;
setTimeout(tick, speed);
} else {
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';
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.textContent = 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 = `
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; },
};
})();