// 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 = '
Thinking…
'; 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); } }); }); });