/* =================================================== JAESWIFT.XYZ — JAE-AI Memory System (Memoria-style) Browser-local persistent memory for chat continuity All data stays in localStorage — never leaves device except as context in the next user message. =================================================== */ (function (global) { 'use strict'; const MEM_KEY = 'jae-ai-memory-v1'; const HIST_KEY = 'jae-ai-history-v1'; const MAX_MEMORIES = 100; const MAX_HISTORY = 50; const EXTRACT_EVERY_N_USER_MSGS = 4; // ─── Storage-safe wrappers ─── function lsGet(key, fallback) { try { const raw = localStorage.getItem(key); if (!raw) return fallback; return JSON.parse(raw); } catch (e) { console.warn('[chat-memory] storage read fail', e); return fallback; } } function lsSet(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); return true; } catch (e) { console.warn('[chat-memory] storage write fail', e); return false; } } // ─── ID helper ─── function uid() { return 'm_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); } // ─── Memory ops ─── function getAll() { return lsGet(MEM_KEY, []); } function saveAll(list) { return lsSet(MEM_KEY, list); } function prune(list) { if (list.length <= MAX_MEMORIES) return list; // sort by importance asc, then timestamp asc (oldest low importance first to drop) const sorted = [...list].sort(function (a, b) { if (a.importance !== b.importance) return a.importance - b.importance; return (a.timestamp || 0) - (b.timestamp || 0); }); const toDrop = list.length - MAX_MEMORIES; const dropIds = new Set(sorted.slice(0, toDrop).map(function (m) { return m.id; })); return list.filter(function (m) { return !dropIds.has(m.id); }); } // Simple normalized tokens for fuzzy matching function tokens(str) { return (str || '') .toLowerCase() .replace(/[^a-z0-9\s]/g, ' ') .split(/\s+/) .filter(function (t) { return t.length > 2; }); } // Jaccard similarity of token sets function similarity(a, b) { const ta = new Set(tokens(a)); const tb = new Set(tokens(b)); if (ta.size === 0 || tb.size === 0) return 0; let inter = 0; ta.forEach(function (t) { if (tb.has(t)) inter++; }); const union = ta.size + tb.size - inter; return union === 0 ? 0 : inter / union; } function add(text, category, importance) { text = (text || '').trim(); if (!text) return null; category = category || 'other'; importance = typeof importance === 'number' ? Math.max(0, Math.min(1, importance)) : 0.5; let list = getAll(); // Dedup: if very similar memory exists, update importance instead of adding for (let i = 0; i < list.length; i++) { if (similarity(list[i].text, text) > 0.8) { list[i].importance = Math.max(list[i].importance || 0, importance); list[i].timestamp = Date.now(); saveAll(list); return list[i]; } } const mem = { id: uid(), text: text, category: category, importance: importance, timestamp: Date.now() }; list.push(mem); list = prune(list); saveAll(list); return mem; } function remove(id) { const list = getAll().filter(function (m) { return m.id !== id; }); saveAll(list); } function clear() { saveAll([]); } // ─── Relevance-ranked search ─── function search(query, limit) { limit = limit || 5; const qTokens = tokens(query); const list = getAll(); if (qTokens.length === 0 || list.length === 0) return []; const scored = list.map(function (m) { const mTokens = tokens(m.text); const mSet = new Set(mTokens); let overlap = 0; qTokens.forEach(function (t) { if (mSet.has(t)) overlap++; }); // substring bonus const lowerText = m.text.toLowerCase(); const lowerQ = (query || '').toLowerCase(); let subBonus = 0; qTokens.forEach(function (t) { if (lowerText.indexOf(t) !== -1) subBonus += 0.3; }); if (lowerQ && lowerText.indexOf(lowerQ) !== -1) subBonus += 1.0; const score = overlap + subBonus + (m.importance || 0) * 0.5; return { mem: m, score: score }; }); scored.sort(function (a, b) { return b.score - a.score; }); return scored.filter(function (s) { return s.score > 0; }).slice(0, limit).map(function (s) { return s.mem; }); } function recent(limit) { limit = limit || 3; return [...getAll()].sort(function (a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }).slice(0, limit); } // ─── Context block for system prompt ─── function getContextBlock(query) { const relevant = search(query, 5); const recentOnes = recent(3); const seen = new Set(relevant.map(function (m) { return m.id; })); const combined = relevant.slice(); recentOnes.forEach(function (m) { if (!seen.has(m.id)) combined.push(m); }); if (combined.length === 0) return ''; const lines = combined.map(function (m) { return '- [' + (m.category || 'other') + '] ' + m.text; }); return '[REMEMBERED FACTS ABOUT THIS USER]\n' + lines.join('\n') + '\n[END REMEMBERED FACTS]'; } // ─── Chat history persistence ─── function getHistory() { return lsGet(HIST_KEY, []); } function saveHistory(messages) { const trimmed = messages.slice(-MAX_HISTORY); return lsSet(HIST_KEY, trimmed); } function appendHistory(msg) { const h = getHistory(); h.push(msg); saveHistory(h); } function clearHistory() { lsSet(HIST_KEY, []); } // ─── Memory extraction via backend ─── let _userMsgCounter = 0; let _extracting = false; function incrementUserMsgAndMaybeExtract() { _userMsgCounter++; if (_userMsgCounter >= EXTRACT_EVERY_N_USER_MSGS) { _userMsgCounter = 0; return extractFromRecentChat(); } return Promise.resolve(null); } function extractFromRecentChat() { if (_extracting) return Promise.resolve(null); _extracting = true; const hist = getHistory().slice(-8); if (hist.length < 2) { _extracting = false; return Promise.resolve(null); } return fetch('/api/chat/extract-memories', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: hist }) }) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (data) { const arr = Array.isArray(data) ? data : (data && Array.isArray(data.memories) ? data.memories : []); const added = []; arr.forEach(function (item) { if (item && typeof item.text === 'string' && item.text.trim()) { const m = add(item.text, item.category || 'other', typeof item.importance === 'number' ? item.importance : 0.5); if (m) added.push(m); } }); _extracting = false; return added; }) .catch(function (e) { console.warn('[chat-memory] extraction failed', e); _extracting = false; return null; }); } // ─── Export JSON ─── function exportJSON() { const data = { version: 1, exported: new Date().toISOString(), memories: getAll(), history: getHistory() }; return JSON.stringify(data, null, 2); } // ─── Public API ─── global.chatMemory = { add: add, getAll: getAll, search: search, remove: remove, clear: clear, getContextBlock: getContextBlock, extractFromRecentChat: extractFromRecentChat, incrementUserMsgAndMaybeExtract: incrementUserMsgAndMaybeExtract, getHistory: getHistory, saveHistory: saveHistory, appendHistory: appendHistory, clearHistory: clearHistory, exportJSON: exportJSON }; })(window);