// NeuralAI Chat UI v4.0 — Full Feature Set with Persistence, Memory, Settings
// Includes: Conversation Management, Memory System, Model Rules, User Bio, Settings
const chatContainer = document.getElementById('chatContainer');
const messagesEl = document.getElementById('messages');
const welcomeScreen = document.getElementById('welcomeScreen');
const chatInput = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn');
const newChatBtn = document.getElementById('newChatBtn');
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const searchInput = document.getElementById('searchInput');
const exportBtn = document.getElementById('exportBtn');
const infoBtn = document.getElementById('infoBtn');
const themeBtn = document.getElementById('themeBtn');
const attachBtn = document.getElementById('attachBtn');
const msgCountEl = document.getElementById('msgCount');
const darkModeBtn = document.getElementById('darkModeBtn');
const queryInput = document.getElementById('queryInput');
const searchBtn = document.getElementById('searchBtn');
// State
let conversation = [];
let isStreaming = false;
let isDark = false;
let attachedFiles = {};
let currentConversationId = null;
let conversations = [];
let userSettings = {};
let memoryFacts = [];
let modelRules = [];
// ========================================
// UTILITIES
// ========================================
function escHtml(text) { return text.replace(/&/g,'&').replace(//g,'>'); }
function fmt(text) {
let out = escHtml(text);
// Convert escaped newlines to actual newlines FIRST
out = out.replace(/\\n/g, '\n');
out = out.replace(/\\t/g, '\t');
out = out.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, l, c) => `
${c.trim()}
`);
out = out.replace(/`([^`]+)`/g, '$1');
out = out.replace(/\*\*([^*]+)\*\*/g, '$1');
out = out.replace(/\*([^*]+)\*/g, '$1');
out = out.replace(/\n/g, '
');
return out;
}
function scrollBottom() { if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight; }
function showToast(msg, type = '') {
const old = document.querySelector('.toast');
if (old) old.remove();
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'} ${msg}`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3500);
}
function formatTime(iso) {
try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
catch { return ''; }
}
// ========================================
// DARK MODE
// ========================================
function toggleDarkMode() {
isDark = !isDark;
document.body.classList.toggle('dark-mode', isDark);
localStorage.setItem('neuralai_dark_mode', isDark);
if (darkModeBtn) darkModeBtn.textContent = isDark ? '☀️ Light' : '🌙 Dark';
updateSetting('theme', isDark ? 'dark' : 'light');
showToast(isDark ? 'Dark mode enabled' : 'Light mode enabled', 'success');
}
function loadDarkMode() {
const saved = localStorage.getItem('neuralai_dark_mode');
if (saved === 'true') {
isDark = true;
document.body.classList.add('dark-mode');
if (darkModeBtn) darkModeBtn.textContent = '☀️ Light';
}
}
darkModeBtn?.addEventListener('click', toggleDarkMode);
themeBtn?.addEventListener('click', toggleDarkMode);
// ========================================
// SETTINGS API
// ========================================
async function loadSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
userSettings = data.settings || {};
} catch (e) {
console.error('Failed to load settings:', e);
}
}
async function updateSetting(key, value) {
try {
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [key]: value })
});
userSettings[key] = value;
} catch (e) {
console.error('Failed to update setting:', e);
}
}
// ========================================
// MEMORY API
// ========================================
async function loadMemory() {
try {
const res = await fetch('/api/memory');
const data = await res.json();
memoryFacts = data.facts || [];
} catch (e) {
console.error('Failed to load memory:', e);
}
}
async function addMemory(fact, category = 'general') {
try {
const res = await fetch('/api/memory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact, category })
});
const data = await res.json();
if (data.success) {
memoryFacts.unshift({ id: data.id, fact, category });
showToast('Memory saved!', 'success');
return true;
}
} catch (e) {
showToast('Failed to save memory', 'error');
}
return false;
}
async function deleteMemory(id) {
try {
await fetch(`/api/memory/${id}`, { method: 'DELETE' });
memoryFacts = memoryFacts.filter(m => m.id !== id);
showToast('Memory deleted', 'success');
} catch (e) {
showToast('Failed to delete memory', 'error');
}
}
// ========================================
// RULES API
// ========================================
async function loadRules() {
try {
const res = await fetch('/api/rules');
const data = await res.json();
modelRules = data.rules || [];
} catch (e) {
console.error('Failed to load rules:', e);
}
}
async function addRule(rule) {
try {
const res = await fetch('/api/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rule, is_active: 1 })
});
const data = await res.json();
if (data.success) {
modelRules.unshift({ id: data.id, rule, is_active: 1 });
showToast('Rule added!', 'success');
return true;
}
} catch (e) {
showToast('Failed to add rule', 'error');
}
return false;
}
async function deleteRule(id) {
try {
await fetch(`/api/rules/${id}`, { method: 'DELETE' });
modelRules = modelRules.filter(r => r.id !== id);
showToast('Rule deleted', 'success');
} catch (e) {
showToast('Failed to delete rule', 'error');
}
}
async function toggleRule(id) {
try {
await fetch(`/api/rules/${id}/toggle`, { method: 'POST' });
const rule = modelRules.find(r => r.id === id);
if (rule) rule.is_active = rule.is_active ? 0 : 1;
} catch (e) {
showToast('Failed to toggle rule', 'error');
}
}
// ========================================
// CONVERSATIONS API
// ========================================
async function loadConversations() {
try {
const res = await fetch('/api/conversations');
const data = await res.json();
conversations = data.conversations || [];
renderHistoryDropdown();
} catch (e) {
console.error('Failed to load conversations:', e);
}
}
async function createNewConversation() {
try {
const res = await fetch('/api/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'New Chat' })
});
const data = await res.json();
if (data.success) {
conversations.unshift({ id: data.id, title: data.title, message_count: 0 });
currentConversationId = data.id;
conversation = [];
messagesEl.innerHTML = '';
welcomeScreen.style.display = 'flex';
renderConversationList();
return data.id;
}
} catch (e) {
showToast('Failed to create conversation', 'error');
}
return null;
}
async function loadConversation(convId) {
try {
const res = await fetch(`/api/conversations/${convId}`);
const data = await res.json();
if (data.conversation) {
currentConversationId = convId;
conversation = data.messages.map(m => ({ role: m.role, content: m.content }));
messagesEl.innerHTML = '';
welcomeScreen.style.display = conversation.length ? 'none' : 'flex';
conversation.forEach(m => addMsg(m.role, m.content));
updateMsgCount();
// Update active state in list
document.querySelectorAll('.history-item').forEach(item => {
item.classList.toggle('active', item.dataset.id === convId);
});
}
} catch (e) {
showToast('Failed to load conversation', 'error');
}
}
async function deleteConversation(convId) {
try {
await fetch(`/api/conversations/${convId}`, { method: 'DELETE' });
conversations = conversations.filter(c => c.id !== convId);
if (currentConversationId === convId) {
currentConversationId = null;
conversation = [];
messagesEl.innerHTML = '';
welcomeScreen.style.display = 'flex';
}
renderConversationList();
showToast('Conversation deleted', 'success');
} catch (e) {
showToast('Failed to delete conversation', 'error');
}
}
function renderConversationList() {
const container = document.getElementById('historyList');
if (!container) return;
container.innerHTML = '';
if (conversations.length === 0) {
container.innerHTML = 'No past conversations
';
return;
}
conversations.forEach(conv => {
const item = document.createElement('div');
item.className = `history-item ${conv.id === currentConversationId ? 'active' : ''}`;
item.dataset.id = conv.id;
item.innerHTML = `
${escHtml(conv.title)}
${conv.message_count || 0} msgs
`;
container.appendChild(item);
});
}
// ========================================
// HISTORY DROPDOWN (in top search bar)
// ========================================
let hideDropdownTimeout = null;
function showHistoryDropdown() {
clearTimeout(hideDropdownTimeout);
const dropdown = document.getElementById('historyDropdown');
if (dropdown) {
dropdown.style.display = 'block';
renderHistoryDropdown();
}
}
function hideHistoryDropdownDelayed() {
hideDropdownTimeout = setTimeout(() => {
const dropdown = document.getElementById('historyDropdown');
if (dropdown) dropdown.style.display = 'none';
}, 200);
}
function renderHistoryDropdown() {
const list = document.getElementById('historyDropdownList');
const empty = document.getElementById('historyDropdownEmpty');
if (!list) return;
list.innerHTML = '';
if (conversations.length === 0) {
list.style.display = 'none';
if (empty) empty.style.display = 'block';
return;
}
list.style.display = 'block';
if (empty) empty.style.display = 'none';
conversations.slice(0, 10).forEach(conv => {
const item = document.createElement('div');
item.className = 'history-item';
item.style.cssText = 'padding:10px 12px;border-bottom:1px solid rgba(255,255,255,0.05);display:flex;justify-content:space-between;align-items:center;';
item.innerHTML = `
${escHtml(conv.title)}
${conv.message_count || 0} msgs
`;
item.onclick = (e) => {
if (e.target.tagName !== 'BUTTON') {
loadConversation(conv.id);
const dropdown = document.getElementById('historyDropdown');
if (dropdown) dropdown.style.display = 'none';
const queryInput = document.getElementById('queryInput');
if (queryInput) queryInput.blur();
}
};
list.appendChild(item);
});
// Add "New Chat" option at bottom
const newChatItem = document.createElement('div');
newChatItem.style.cssText = 'padding:10px 12px;text-align:center;color:#e4e4e7;cursor:pointer;font-weight:500;border-top:1px solid rgba(255,255,255,0.1);';
newChatItem.textContent = '+ New Chat';
newChatItem.onclick = () => {
createNewConversation();
const dropdown = document.getElementById('historyDropdown');
if (dropdown) dropdown.style.display = 'none';
};
list.appendChild(newChatItem);
}
window.showHistoryDropdown = showHistoryDropdown;
window.hideHistoryDropdownDelayed = hideHistoryDropdownDelayed;
// ========================================
// INFO PANEL
// ========================================
infoBtn?.addEventListener('click', () => {
fetch('/api/status').then(r => r.json()).then(data => {
const m = document.createElement('div');
m.style.cssText = 'position:fixed;top:70px;right:16px;width:320px;background:var(--surface,#0f0f17);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;z-index:200;box-shadow:0 8px 32px rgba(0,0,0,0.5);';
m.innerHTML = `
🧠 NeuralAI v${data.version}
Base: ${data.model||'SmolLM2-360M-Instruct'}
Type: ${data.model_type||'base'}
Device: ${data.device||'CPU'}
RAG: ${data.rag?'✅ Active':'❌ Off'}
Files: ${data.indexed_files||0}
Memory: ${memoryFacts.length} facts
Rules: ${modelRules.filter(r=>r.is_active).length} active
`;
document.body.appendChild(m);
setTimeout(() => m.remove(), 8000);
});
});
// ========================================
// MEMORY PANEL
// ========================================
function showMemoryPanel() {
const existing = document.getElementById('memoryPanel');
if (existing) { existing.remove(); return; }
const panel = document.createElement('div');
panel.id = 'memoryPanel';
panel.style.cssText = 'position:fixed;top:0;right:0;width:400px;height:100vh;background:#0f0f17;border-left:1px solid rgba(255,255,255,0.1);z-index:1000;overflow-y:auto;padding:24px;';
panel.innerHTML = `
🧠 Memory
Saved Memories:
`;
document.body.appendChild(panel);
renderMemoryList();
document.getElementById('addMemoryBtn').addEventListener('click', async () => {
const input = document.getElementById('newMemoryInput');
const fact = input.value.trim();
if (fact && await addMemory(fact)) {
input.value = '';
renderMemoryList();
}
});
}
function renderMemoryList() {
const container = document.getElementById('memoryList');
if (!container) return;
container.innerHTML = '';
if (memoryFacts.length === 0) {
container.innerHTML = 'No memories saved yet
';
return;
}
memoryFacts.forEach(m => {
const item = document.createElement('div');
item.style.cssText = 'display:flex;justify-content:space-between;align-items:flex-start;background:#161b22;border-radius:8px;padding:12px;';
item.innerHTML = `
${escHtml(m.fact)}
`;
container.appendChild(item);
});
}
// ========================================
// RULES PANEL
// ========================================
function showRulesPanel() {
const existing = document.getElementById('rulesPanel');
if (existing) { existing.remove(); return; }
const panel = document.createElement('div');
panel.id = 'rulesPanel';
panel.style.cssText = 'position:fixed;top:0;right:0;width:400px;height:100vh;background:#0f0f17;border-left:1px solid rgba(255,255,255,0.1);z-index:1000;overflow-y:auto;padding:24px;';
panel.innerHTML = `
📜 Model Rules
Rules guide how the AI responds. Active rules are injected into the system prompt.
Active Rules:
`;
document.body.appendChild(panel);
renderRulesList();
document.getElementById('addRuleBtn').addEventListener('click', async () => {
const input = document.getElementById('newRuleInput');
const rule = input.value.trim();
if (rule && await addRule(rule)) {
input.value = '';
renderRulesList();
}
});
}
function renderRulesList() {
const container = document.getElementById('rulesList');
if (!container) return;
container.innerHTML = '';
if (modelRules.length === 0) {
container.innerHTML = 'No rules defined yet
';
return;
}
modelRules.forEach(r => {
const item = document.createElement('div');
item.style.cssText = `display:flex;justify-content:space-between;align-items:center;background:#161b22;border-radius:8px;padding:12px;opacity:${r.is_active ? 1 : 0.5}`;
item.innerHTML = `
${escHtml(r.rule)}
`;
container.appendChild(item);
});
}
// Make functions global for onclick
window.showMemoryPanel = showMemoryPanel;
window.showRulesPanel = showRulesPanel;
window.loadConversation = loadConversation;
window.deleteConversation = deleteConversation;
window.deleteMemory = deleteMemory;
window.toggleRule = toggleRule;
window.deleteRule = deleteRule;
window.renderMemoryList = renderMemoryList;
window.renderRulesList = renderRulesList;
// ========================================
// EXPORT
// ========================================
exportBtn?.addEventListener('click', () => {
if (conversation.length === 0) { showToast('No conversation to export', 'error'); return; }
const md = conversation.filter(m => m.role !== 'system').map(m => `**${m.role}:** ${m.content}`).join('\n\n');
const blob = new Blob([`# NeuralAI Chat\n\n${md}\n\n*Exported: ${new Date().toLocaleString()}*`], { type: 'text/markdown' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `neuralai-chat-${Date.now()}.md`; a.click();
URL.revokeObjectURL(a.href);
showToast('Chat exported!', 'success');
});
// ========================================
// SIDEBAR
// ========================================
function openSidebar() { sidebar.classList.add('open'); sidebarOverlay.classList.add('open'); }
function closeSidebar() { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('open'); }
sidebarToggle?.addEventListener('click', openSidebar);
sidebarOverlay?.addEventListener('click', closeSidebar);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSidebar(); });
// ========================================
// SEARCH
// ========================================
searchInput?.addEventListener('input', () => {
const q = searchInput.value.toLowerCase();
document.querySelectorAll('.history-item').forEach(item => {
const t = item.querySelector('.history-item-text')?.textContent || '';
item.style.display = t.toLowerCase().includes(q) ? 'flex' : 'none';
});
});
// ========================================
// FILE UPLOAD
// ========================================
function updateAttachedUI() {
const existing = document.getElementById('attachedFiles');
if (existing) existing.remove();
const ids = Object.keys(attachedFiles);
if (ids.length === 0) return;
const div = document.createElement('div');
div.id = 'attachedFiles';
div.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 16px;background:var(--surface-2,#14141f);border-top:1px solid var(--border,rgba(255,255,255,0.06));';
ids.forEach(fid => {
const name = attachedFiles[fid];
const chip = document.createElement('span');
chip.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:var(--accent-glow,rgba(59,130,246,0.15));border:1px solid rgba(59,130,246,0.3);border-radius:20px;font-size:11px;color:var(--accent-text,#e4e4e7);';
chip.textContent = '📄 ' + (name.length > 20 ? name.slice(0,18) + '…' : name);
div.appendChild(chip);
});
document.querySelector('.input-area')?.parentElement?.insertBefore(div, document.querySelector('.input-area'));
}
attachBtn?.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.docx,.doc,.txt,.md';
input.onchange = async () => {
if (!input.files[0]) return;
const file = input.files[0];
const formData = new FormData();
formData.append('file', file);
showToast(`📄 Uploading "${file.name}"…`);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.error) { showToast('❌ ' + data.error, 'error'); return; }
attachedFiles[data.file_id] = data.filename;
showToast(data.message, 'success');
updateAttachedUI();
updateMsgCount();
} catch (err) { showToast('❌ Upload failed: ' + err.message, 'error'); }
};
input.click();
});
// ========================================
// NEW CHAT
// ========================================
newChatBtn?.addEventListener('click', async () => {
await createNewConversation();
chatInput.value = '';
chatInput.style.height = 'auto';
attachedFiles = {};
const af = document.getElementById('attachedFiles');
if (af) af.remove();
chatInput.dispatchEvent(new Event('input'));
closeSidebar();
updateMsgCount();
});
// ========================================
// INPUT HANDLING
// ========================================
chatInput?.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 160) + 'px';
sendBtn.disabled = chatInput.value.trim() === '' || isStreaming;
});
chatInput?.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!sendBtn.disabled) sendMessage(); }
});
sendBtn?.addEventListener('click', () => { if (!sendBtn.disabled) sendMessage(); });
// Quick prompts
document.querySelectorAll('.prompt-card').forEach(btn => {
btn.addEventListener('click', () => {
chatInput.value = btn.dataset.prompt;
chatInput.dispatchEvent(new Event('input'));
sendMessage();
});
});
// ========================================
// SEND MESSAGE
// ========================================
async function sendMessage() {
const userMsg = chatInput.value.trim();
if (!userMsg || isStreaming) return;
welcomeScreen.style.display = 'none';
const fileIdsThisMsg = Object.keys(attachedFiles);
// Create conversation if needed
if (!currentConversationId) {
await createNewConversation();
}
addMsg('user', userMsg, fileIdsThisMsg);
conversation.push({ role: 'user', content: userMsg });
chatInput.value = '';
chatInput.style.height = 'auto';
chatInput.dispatchEvent(new Event('input'));
updateMsgCount();
isStreaming = true;
sendBtn.disabled = false;
sendBtn.innerHTML = '';
const assistantEl = addMsg('assistant', '');
const bubbleEl = assistantEl.querySelector('.msg-bubble');
bubbleEl.innerHTML = '';
scrollBottom();
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: userMsg,
temperature: parseFloat(userSettings.model_temperature || 0.7),
max_tokens: parseInt(userSettings.model_max_tokens || 512),
messages: conversation,
file_ids: fileIdsThisMsg,
conversation_id: currentConversationId
})
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const reader = res.body.getReader();
const dec = new TextDecoder();
let full = '';
bubbleEl.innerHTML = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = dec.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const raw = line.slice(6).trim();
if (raw === '[DONE]') {
isStreaming = false;
break;
}
try {
const parsed = JSON.parse(raw);
if (parsed.content) {
// Unescape the content - handle double-escaped strings
let content = parsed.content;
// If content has escaped backslashes, unescape them
content = content.replace(/\\\\n/g, '\\n');
full += content;
bubbleEl.innerHTML = fmt(full) + copyBtn();
scrollBottom();
}
} catch (e) {
console.error('Parse error:', e, 'for:', raw.substring(0, 50));
}
}
}
}
conversation.push({ role: 'assistant', content: full });
// Refresh conversation list to update title/count
loadConversations();
} catch (err) {
bubbleEl.innerHTML = '⚠️ Error: ' + err.message + '';
} finally {
isStreaming = false;
sendBtn.disabled = chatInput.value.trim() === '';
sendBtn.innerHTML = '';
scrollBottom();
closeSidebar();
}
}
// ========================================
// ADD MESSAGE
// ========================================
function addMsg(role, content, fileIds) {
const div = document.createElement('div');
div.className = 'message ' + role;
let attachedInfo = '';
if (role === 'user' && fileIds && fileIds.length > 0) {
const names = fileIds.map(fid => attachedFiles[fid] || fid);
attachedInfo = '' +
names.map(n => '📄 ' + escHtml(n.length > 22 ? n.slice(0,20)+'…' : n) + '').join('') + '
';
}
const avatar = role === 'assistant'
? ''
: '';
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
div.innerHTML = avatar + '' + (role === 'assistant' ? 'NeuralAI' : 'You') + '' + time + '
' + (content ? escHtml(content) : '') + '
' + attachedInfo + '
';
messagesEl.appendChild(div);
scrollBottom();
return div;
}
function copyBtn() {
return '';
}
window.copyMsg = function(btn) {
const text = btn.closest('.msg-bubble').innerText.replace(/Copy$/, '').trim();
navigator.clipboard.writeText(text).then(() => {
btn.innerHTML = 'Copied!';
setTimeout(() => { btn.innerHTML = 'Copy'; }, 2000);
});
};
function updateMsgCount() {
if (msgCountEl) msgCountEl.textContent = conversation.filter(m => m.role !== 'system').length + ' messages';
}
// ========================================
// INITIALIZATION
// ========================================
async function initialize() {
loadDarkMode();
await loadSettings();
await loadMemory();
await loadRules();
await loadConversations();
chatInput?.dispatchEvent(new Event('input'));
console.log('[NeuralAI] v4.0 initialized with persistence, memory, and settings');
}
initialize();
// Build: 1777526000
// ========================================
// SETTINGS TAB FUNCTIONS
// ========================================
async function saveUserBio() {
const bio = document.getElementById('userBioInput').value.trim();
try {
await updateSetting('user_bio', bio);
showToast('Bio saved!', 'success');
} catch (e) {
showToast('Failed to save bio', 'error');
}
}
async function loadBio() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
const bioInput = document.getElementById('userBioInput');
if (bioInput && data.settings && data.settings.user_bio) {
bioInput.value = data.settings.user_bio;
}
} catch (e) {
console.error('Failed to load bio:', e);
}
}
async function addMemoryFromTab() {
const input = document.getElementById('memoryInput');
const fact = input.value.trim();
if (!fact) return;
try {
await fetch('/api/memory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact })
});
input.value = '';
loadMemoryList();
showToast('Memory added!', 'success');
} catch (e) {
showToast('Failed to add memory', 'error');
}
}
async function loadMemoryList() {
try {
const res = await fetch('/api/memory');
const data = await res.json();
const list = document.getElementById('memoryList');
if (!list) return;
list.innerHTML = '';
(data.facts || []).forEach(m => {
const div = document.createElement('div');
div.className = 'memory-item';
div.innerHTML = `
${escHtml(m.fact)}
`;
list.appendChild(div);
});
} catch (e) {
console.error('Failed to load memory:', e);
}
}
async function deleteMemoryItem(id) {
try {
await fetch(`/api/memory/${id}`, { method: 'DELETE' });
loadMemoryList();
showToast('Memory deleted', 'success');
} catch (e) {
showToast('Failed to delete memory', 'error');
}
}
async function addRuleFromTab() {
const input = document.getElementById('ruleInput');
const rule = input.value.trim();
if (!rule) return;
try {
await fetch('/api/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rule, is_active: 1 })
});
input.value = '';
loadRulesList();
showToast('Rule added!', 'success');
} catch (e) {
showToast('Failed to add rule', 'error');
}
}
async function loadRulesList() {
try {
const res = await fetch('/api/rules');
const data = await res.json();
const list = document.getElementById('rulesList');
if (!list) return;
list.innerHTML = '';
(data.rules || []).forEach(r => {
const div = document.createElement('div');
div.className = 'rule-item';
div.innerHTML = `
${escHtml(r.rule)}
`;
list.appendChild(div);
});
} catch (e) {
console.error('Failed to load rules:', e);
}
}
async function toggleRuleItem(id, currentState) {
try {
await fetch(`/api/rules/${id}/toggle`, { method: 'POST' });
loadRulesList();
} catch (e) {
showToast('Failed to toggle rule', 'error');
}
}
async function deleteRuleItem(id) {
try {
await fetch(`/api/rules/${id}`, { method: 'DELETE' });
loadRulesList();
showToast('Rule deleted', 'success');
} catch (e) {
showToast('Failed to delete rule', 'error');
}
}
// Make functions global for onclick handlers
window.saveUserBio = saveUserBio;
window.addMemoryFromTab = addMemoryFromTab;
window.loadMemoryList = loadMemoryList;
window.deleteMemoryItem = deleteMemoryItem;
window.addRuleFromTab = addRuleFromTab;
window.loadRulesList = loadRulesList;
window.toggleRuleItem = toggleRuleItem;
window.deleteRuleItem = deleteRuleItem;
// Load settings when tab is shown
document.addEventListener('DOMContentLoaded', () => {
// Watch for settings tab
document.querySelectorAll('.nav-item[data-tab]').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.tab === 'settings') {
setTimeout(() => {
loadBio();
loadMemoryList();
loadRulesList();
}, 100);
}
});
});
});