/* =================================================== JAESWIFT.XYZ — JAE-AI Chat Terminal Venice API chat interface + Memoria-style memory =================================================== */ (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; // ─── 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; } // ─── 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 || 12; 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(); }); } // ─── Restore previous chat history from localStorage ─── function restoreHistory() { if (!mem) return false; const saved = mem.getHistory(); if (!saved || saved.length === 0) return false; // Remove welcome and render saved history 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); }); // Divider showing restore const divider = document.createElement('div'); divider.className = 'chat-divider'; divider.innerHTML = '— RESTORED FROM MEMORY —'; chatMessages.appendChild(divider); chatMessages.scrollTop = chatMessages.scrollHeight; // populate in-memory history history = saved.slice(); return true; } // ─── Send message to API ─── async function sendMessage() { const text = chatInput.value.trim(); if (!text || isWaiting) 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 memory context block const memoryBlock = mem ? mem.getContextBlock(text) : ''; try { const resp = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, history: history.slice(0, -1), memory_context: memoryBlock }) }); hideTyping(); if (!resp.ok) { const err = await resp.json().catch(function () { return {}; }); addMessage('assistant', 'ERROR: ' + (err.error || 'Connection failed')); chatStatus.textContent = '● ERROR'; chatStatus.classList.remove('status-amber'); chatStatus.classList.add('status-red'); isWaiting = false; return; } const data = await resp.json(); const reply = data.reply || 'No response received.'; 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', ''); await typewriterEffect(bodyEl, reply, 10); // Trigger memory extraction every N user messages (non-blocking) 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(); // toggle off/on to refresh }); }); 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(); } }); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; }); } // ─── Inject memory button + privacy info into chat header ─── 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(); // Update memory count periodically setInterval(updateMemCount, 5000); const restored = restoreHistory(); if (!restored) { // Auto-greeting after short delay (only if no history restored) setTimeout(function () { showTyping(); setTimeout(function () { hideTyping(); var greeting = 'Welcome to JAESWIFT.XYZ — I\'m JAE-AI, your onboard guide. Ask me anything about this system, or try saying "what can I explore here?"'; history.push({ role: 'assistant', content: greeting }); if (mem) mem.appendHistory({ role: 'assistant', content: greeting }); var bodyEl = addMessage('assistant', ''); typewriterEffect(bodyEl, greeting, 15); }, 1500); }, 2000); } })();