Instructions to use Subject-Emu-5259/NeuralAI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Subject-Emu-5259/NeuralAI with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| /** | |
| * NeuralAI Core UI Engine v6.3 | |
| * Unified script handling Auth, Chat, Memory, Rules, Settings, Terminal and NeuralDrive. | |
| */ | |
| // ======================================== | |
| // GLOBAL STATE | |
| // ======================================== | |
| let authToken = window.localStorage.getItem("neural_token"); | |
| let currentUser = null; | |
| let currentConversationId = null; | |
| let isStreaming = false; | |
| let abortStream = false; | |
| let lastSeen = {}; | |
| let conversation = []; | |
| let attachedFiles = {}; | |
| let uplinkEnabled = false; | |
| let termSid = null; | |
| let termPoll = null; | |
| let authMode = "login"; | |
| let currentShell = "bash"; | |
| let searchMode = "all"; | |
| let isInitializingTerm = false; | |
| let userSettings = { theme: 'dark', neural_voice: 'Andrew', user_bio: '' }; | |
| let voiceWS = null; | |
| let audioCtx = null; | |
| let processor = null; | |
| let micStream = null; | |
| let audioQueue = []; | |
| let isPlaying = false; | |
| let nextPlayTime = 0; | |
| let voiceReconnectTimer = null; | |
| // ======================================== | |
| // AUTHENTICATION | |
| // ======================================== | |
| async function initAuth() { | |
| console.log("Initializing Auth state..."); | |
| const logoutBtn = document.getElementById("logoutBtn"); | |
| const authOverlay = document.getElementById("authOverlay"); | |
| if (!authToken) { | |
| console.log("No token found, showing auth overlay."); | |
| if (logoutBtn) logoutBtn.classList.add("hidden"); | |
| if (authOverlay) authOverlay.classList.remove("hidden"); | |
| return; | |
| } | |
| try { | |
| const r = await fetch("/api/user/me", { | |
| headers: { "Authorization": `Bearer ${authToken}` } | |
| }); | |
| if (!r.ok) { | |
| console.warn("Auth token invalid or expired."); | |
| throw new Error("Invalid token"); | |
| } | |
| const data = await r.json(); | |
| currentUser = data.user; | |
| const greeting = document.getElementById("userGreeting"); | |
| if (greeting) { | |
| greeting.textContent = currentUser.account_type === "guest" | |
| ? `Guest Session` | |
| : `Hi, ${currentUser.username}`; | |
| } | |
| if (logoutBtn) logoutBtn.classList.remove("hidden"); | |
| if (authOverlay) authOverlay.classList.add("hidden"); | |
| loadConversations(); | |
| loadUserProfile(); | |
| } catch (err) { | |
| console.error("Auth check failed:", err); | |
| logout(); | |
| } | |
| } | |
| function showAuth() { | |
| const authOverlay = document.getElementById("authOverlay"); | |
| if (authOverlay) authOverlay.classList.remove("hidden"); | |
| const logoutBtn = document.getElementById("logoutBtn"); | |
| if (logoutBtn) logoutBtn.classList.add("hidden"); | |
| } | |
| function logout() { | |
| console.log("Logging out..."); | |
| window.localStorage.removeItem("neural_token"); | |
| authToken = null; | |
| currentUser = null; | |
| const logoutBtn = document.getElementById("logoutBtn"); | |
| if (logoutBtn) logoutBtn.classList.add("hidden"); | |
| const greeting = document.getElementById("userGreeting"); | |
| if (greeting) greeting.textContent = ""; | |
| const authOverlay = document.getElementById("authOverlay"); | |
| if (authOverlay) authOverlay.classList.remove("hidden"); | |
| // Clear local state | |
| conversation = []; | |
| currentConversationId = null; | |
| document.getElementById('messages').innerHTML = ''; | |
| // Optional: location.href = "/"; // Only if we want a full clean state | |
| } | |
| function toggleAuthMode() { | |
| authMode = authMode === "login" ? "signup" : "login"; | |
| const title = document.getElementById("authTitle"); | |
| const subtitle = document.getElementById("authSubtitle"); | |
| const submit = document.getElementById("authSubmit"); | |
| const toggle = document.getElementById("authToggle"); | |
| const emailGroup = document.getElementById("emailGroup"); | |
| const usernameGroup = document.getElementById("usernameGroup"); | |
| const confirmGroup = document.getElementById("confirmPasswordGroup"); | |
| if (authMode === "signup") { | |
| if (title) title.textContent = "Create Account"; | |
| if (subtitle) subtitle.textContent = "Join the NeuralAI network and start building."; | |
| if (submit) submit.textContent = "Sign Up"; | |
| if (toggle) toggle.innerHTML = "Already have an account? <span>Login</span>"; | |
| if (emailGroup) emailGroup.classList.remove("hidden"); | |
| if (usernameGroup) usernameGroup.classList.remove("hidden"); | |
| if (confirmGroup) confirmGroup.classList.remove("hidden"); | |
| } else { | |
| if (title) title.textContent = "Welcome Back"; | |
| if (subtitle) subtitle.textContent = "Enter your credentials to access your cloud brain."; | |
| if (submit) submit.textContent = "Login"; | |
| if (toggle) toggle.innerHTML = "Don't have an account? <span>Sign Up</span>"; | |
| if (emailGroup) emailGroup.classList.remove("hidden"); | |
| if (usernameGroup) usernameGroup.classList.add("hidden"); | |
| if (confirmGroup) confirmGroup.classList.add("hidden"); | |
| } | |
| } | |
| async function handleAuth() { | |
| const identity = document.getElementById("authEmail")?.value.trim(); | |
| const username = document.getElementById("authUsername")?.value.trim(); | |
| const password = document.getElementById("authPassword")?.value.trim(); | |
| const confirm = document.getElementById("authConfirmPassword")?.value.trim(); | |
| if (!identity || !password) return showToast("Credentials required", "error"); | |
| if (authMode === "signup" && !username) return showToast("Username required for signup", "error"); | |
| if (authMode === "signup" && password !== confirm) return showToast("Passwords do not match", "error"); | |
| const url = authMode === "signup" ? "/api/auth/signup" : "/api/auth/login"; | |
| const body = authMode === "signup" ? { username, password, email: identity } : { username: identity, password }; | |
| try { | |
| const res = await fetch(url, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", "Accept": "application/json" }, | |
| body: JSON.stringify(body) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || "Authentication failed"); | |
| if (authMode === "signup") { | |
| showToast("Account created! Please login.", "success"); | |
| toggleAuthMode(); | |
| } else { | |
| window.localStorage.setItem("neural_token", data.token); | |
| authToken = data.token; | |
| showToast("Access granted", "success"); | |
| document.getElementById("authOverlay").classList.add("hidden"); | |
| const logoutBtn = document.getElementById("logoutBtn"); | |
| if (logoutBtn) logoutBtn.classList.remove("hidden"); | |
| if (data.user) { | |
| currentUser = data.user; | |
| const greeting = document.getElementById("userGreeting"); | |
| if (greeting) greeting.textContent = `Hi, ${currentUser.username}`; | |
| } | |
| loadConversations(); | |
| } | |
| } catch (err) { | |
| showToast(err.message, "error"); | |
| } | |
| } | |
| async function handleMaestroAuth() { | |
| showToast("Maestro Auth Triggered", "info"); | |
| const codeEl = document.getElementById("maestroCode"); | |
| const code = codeEl?.value?.trim(); | |
| if (!code) return showToast("Maestro Student ID required", "error"); | |
| try { | |
| showToast("Validating Maestro Pattern...", "info"); | |
| const res = await fetch("/api/auth/maestro", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ invite_code: code }) | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || "Access denied"); | |
| window.localStorage.setItem("neural_token", data.token); | |
| authToken = data.token; | |
| showToast(`Welcome, Maestrian ${data.user?.username || 'Student'}`, "success"); | |
| document.getElementById("authOverlay")?.classList.add("hidden"); | |
| document.getElementById("logoutBtn")?.classList.remove("hidden"); | |
| if (data.user) { | |
| currentUser = data.user; | |
| const greeting = document.getElementById("userGreeting"); | |
| if (greeting) greeting.textContent = `Hi, ${currentUser.username}`; | |
| } | |
| loadConversations(); | |
| } catch (err) { | |
| console.error("Maestro Auth Error:", err); | |
| showToast(err.message || "Authentication failed", "error"); | |
| } | |
| } | |
| async function handleGuestAuth() { | |
| showToast("Guest Auth Triggered", "info"); | |
| try { | |
| showToast("Generating Guest Session...", "info"); | |
| const res = await fetch("/api/auth/guest", { method: "POST" }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || "Guest access failed"); | |
| window.localStorage.setItem("neural_token", data.token); | |
| authToken = data.token; | |
| showToast(`Session active as ${data.user?.username || 'Guest'}`, "success"); | |
| document.getElementById("authOverlay")?.classList.add("hidden"); | |
| document.getElementById("logoutBtn")?.classList.remove("hidden"); | |
| if (data.user) { | |
| currentUser = data.user; | |
| const greeting = document.getElementById("userGreeting"); | |
| if (greeting) greeting.textContent = `Guest Session: ${currentUser.username}`; | |
| } | |
| loadConversations(); | |
| } catch (err) { | |
| console.error("Guest Auth Error:", err); | |
| showToast(err.message || "Authentication failed", "error"); | |
| } | |
| } | |
| // ======================================== | |
| // CHAT & CONVERSATIONS | |
| // ======================================== | |
| async function loadConversations() { | |
| try { | |
| const res = await fetch('/api/conversations', { | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| const data = await res.json(); | |
| // Support both {conversations: []} and direct array [] | |
| const convs = Array.isArray(data) ? data : (data.conversations || []); | |
| renderConversationList(convs); | |
| } 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', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ title: 'New Conversation' }) | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| throw new Error(err.error || 'Server error'); | |
| } | |
| const data = await res.json(); | |
| if (!data.id) throw new Error('No ID returned'); | |
| currentConversationId = data.id; | |
| conversation = []; | |
| const msgsEl = document.getElementById('messages'); | |
| if (msgsEl) msgsEl.innerHTML = ''; | |
| const welcome = document.getElementById('welcomeScreen'); | |
| if (welcome) welcome.style.display = 'flex'; | |
| await loadConversations(); | |
| return data.id; | |
| } catch (e) { | |
| showToast('Failed to create chat: ' + e.message, 'error'); | |
| throw e; | |
| } | |
| } | |
| async function loadConversation(id) { | |
| currentConversationId = id; | |
| lastSeen[id] = new Date().toISOString(); | |
| try { | |
| const res = await fetch(`/api/conversations/${id}`, { | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (!res.ok) throw new Error('Failed to fetch chat'); | |
| const data = await res.json(); | |
| conversation = data.messages || []; | |
| const container = document.getElementById('messages'); | |
| container.innerHTML = ''; | |
| document.getElementById('welcomeScreen').style.display = 'none'; | |
| conversation.forEach(m => addMsg(m.role, m.content)); | |
| loadConversations(); | |
| } catch (e) { showToast('Failed to load history', 'error'); } | |
| } | |
| async function deleteConversation(id) { | |
| if (!confirm('Purge this intelligence log permanently?')) return; | |
| try { | |
| await fetch(`/api/conversations/${id}`, { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (currentConversationId === id) { | |
| currentConversationId = null; | |
| conversation = []; | |
| document.getElementById('messages').innerHTML = ''; | |
| document.getElementById('welcomeScreen').style.display = 'flex'; | |
| } | |
| loadConversations(); | |
| } catch (e) { showToast('Failed to delete', 'error'); } | |
| } | |
| async function renameConversation(id) { | |
| const newTitle = prompt('Enter new name for this chat:'); | |
| if (!newTitle || newTitle.trim() === '') return; | |
| try { | |
| const res = await fetch(`/api/conversations/${id}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ title: newTitle.trim() }) | |
| }); | |
| if (!res.ok) throw new Error('Rename failed'); | |
| showToast('Chat renamed', 'success'); | |
| loadConversations(); | |
| } catch (e) { showToast('Failed to rename', 'error'); } | |
| } | |
| function renderConversationList(convs) { | |
| const list = document.getElementById('sidebarHistoryList'); | |
| if (!list) return; | |
| list.innerHTML = convs.map(c => { | |
| const isUnread = c.id !== currentConversationId && c.updated_at && lastSeen[c.id] && c.updated_at > lastSeen[c.id]; | |
| return ` | |
| <div class="history-item ${currentConversationId === c.id ? 'active' : ''}" onclick="loadConversation('${c.id}')"> | |
| <span class="history-item-text">${escHtml(c.title)}</span> | |
| ${isUnread ? '<span class="unread-badge">NEW</span>' : ''} | |
| <div class="history-item-actions"> | |
| <button class="history-item-rename" onclick="event.stopPropagation(); renameConversation('${c.id}')">✏️</button> | |
| <button class="history-item-delete" onclick="event.stopPropagation(); deleteConversation('${c.id}')">×</button> | |
| </div> | |
| </div> | |
| `}).join('') || '<p style="font-size:12px;color:#555;padding:10px;">No logs found.</p>'; | |
| // Track last-seen timestamps for unread detection | |
| convs.forEach(c => { if (!lastSeen[c.id]) lastSeen[c.id] = c.updated_at; }); | |
| // Update dropdown too | |
| const dropList = document.getElementById('historyDropdownList'); | |
| if (dropList) { | |
| dropList.innerHTML = convs.slice(0, 5).map(c => ` | |
| <div class="history-item" onclick="loadConversation('${c.id}'); hideHistoryDropdown();" style="border-radius:0;border-bottom:1px solid rgba(255,255,255,0.05);"> | |
| <span class="history-item-text">${escHtml(c.title)}</span> | |
| </div> | |
| `).join(''); | |
| const emptyMsg = document.getElementById('historyDropdownEmpty'); | |
| if (emptyMsg) emptyMsg.style.display = convs.length === 0 ? 'block' : 'none'; | |
| } | |
| } | |
| async function sendMessage(textOverride = null) { | |
| console.log("SendMessage called. Override:", textOverride); | |
| const input = document.getElementById('chatInput'); | |
| const text = textOverride || (input ? input.value.trim() : ""); | |
| if (!text) { | |
| console.warn("SendMessage: No text to send"); | |
| return; | |
| } | |
| // Intent detection: trigger image/code "upon request" instead of a composer button | |
| const lower = text.toLowerCase(); | |
| // Image intent: a generation verb + a subject, OR any explicit image noun. | |
| // Covers "generate a dog", "make a cat", "draw a sunset", "image of a car", etc. | |
| const imageVerb = /(generat|creat|make|draw|render|paint|produce|show me|give me|design)/.test(lower); | |
| const imageNoun = /\b(image|picture|photo|drawing|render|painting|pic|gif|meme)\b/.test(lower); | |
| const imageIntent = (imageVerb && lower.length > 3) || imageNoun; | |
| const codeIntent = /\b(run|execute|eval|compute|calculate|code|python|script|function|def |print\()/.test(lower) && /(run|execute|this|code|script|calculate|compute)/.test(lower); | |
| if (imageIntent && !lower.includes('chat') && !lower.includes('explain')) { | |
| if (!textOverride && input) input.value = ''; | |
| generateImageFromPrompt(text); | |
| return; | |
| } | |
| if (codeIntent) { | |
| const modal = document.getElementById('codeModal'); | |
| const editor = document.getElementById('codeEditor'); | |
| if (modal && editor) { | |
| // Extract a fenced code block if present, else use the whole prompt | |
| const fence = text.match(/```(?:python)?\s*([\s\S]*?)```/i); | |
| editor.value = fence ? fence[1].trim() : text.replace(/run (this )?code:?/i, '').trim(); | |
| modal.classList.remove('hidden'); | |
| editor.focus(); | |
| if (!textOverride && input) input.value = ''; | |
| return; | |
| } | |
| } | |
| if (isStreaming) { | |
| console.warn("SendMessage: Already streaming"); | |
| return; | |
| } | |
| try { | |
| if (!currentConversationId) { | |
| console.log("No current conversation. Creating one..."); | |
| await createNewConversation(); | |
| } | |
| } catch (e) { | |
| console.error("Conversation creation failed:", e); | |
| showToast("Failed to initialize intelligence log", "error"); | |
| return; | |
| } | |
| if (!textOverride && input) { | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| } | |
| const welcome = document.getElementById('welcomeScreen'); | |
| if (welcome) welcome.style.display = 'none'; | |
| addMsg('user', text); | |
| conversation.push({ role: 'user', content: text }); | |
| isStreaming = true; | |
| const sendBtn = document.getElementById('sendBtn'); | |
| if (sendBtn) { | |
| sendBtn.disabled = true; | |
| sendBtn.innerHTML = '●●●'; | |
| } | |
| const assistantMsg = addMsg('assistant', ''); | |
| const bubble = assistantMsg.querySelector('.msg-bubble'); | |
| bubble.innerHTML = '<div class="thinking-dots"><span></span><span></span><span></span></div>'; | |
| const typingLabel = assistantMsg.querySelector('.typing-label'); | |
| if (typingLabel) typingLabel.style.display = 'block'; | |
| abortStream = false; | |
| showStopButton(true); | |
| try { | |
| const res = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ prompt: text, conversation_id: currentConversationId, messages: conversation, use_uplink: uplinkEnabled }) | |
| }); | |
| if (!res.ok) { | |
| const errData = await res.json().catch(() => ({})); | |
| throw new Error(errData.error || `Server responded with ${res.status}`); | |
| } | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let full = ''; | |
| let firstToken = true; | |
| bubble.innerHTML = ''; | |
| while (true) { | |
| if (abortStream) { | |
| await fetch('/api/chat/stop', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ conversation_id: currentConversationId }) | |
| }).catch(() => {}); | |
| break; | |
| } | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const raw = line.slice(6).trim(); | |
| if (raw === '[DONE]') break; | |
| try { | |
| const data = JSON.parse(raw); | |
| if (data.content) { | |
| if (firstToken) { | |
| firstToken = false; | |
| if (typingLabel) typingLabel.style.display = 'none'; | |
| } | |
| full += data.content; | |
| // Render tool-cards when an uplink agent emits a 🔧 marker | |
| if (data.content.includes('🔧') || full.includes('🔧 NeuralAI is processing tool')) { | |
| bubble.innerHTML = renderToolCard(full); | |
| } else { | |
| bubble.innerHTML = fmt(full); | |
| } | |
| const msgs = document.getElementById('messages'); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } | |
| } catch {} | |
| } | |
| } | |
| } | |
| conversation.push({ role: 'assistant', content: full }); | |
| // Auto-update 'Recent Intelligence' (sidebar) on first reply to pick up the auto-generated title | |
| if (conversation.length <= 2) { | |
| console.log("First interaction complete. Refreshing sidebar..."); | |
| setTimeout(loadConversations, 500); // Small delay to allow DB commit | |
| } | |
| // Send to Voice TTS if Live Voice is active | |
| if (voiceWS && voiceWS.readyState === WebSocket.OPEN) { | |
| console.log('Sending text to voice engine:', full.substring(0, 50) + '...'); | |
| voiceWS.send(JSON.stringify({ type: 'text', data: full })); | |
| } else { | |
| console.warn('Voice WebSocket is not open. State:', voiceWS ? voiceWS.readyState : 'null'); | |
| if (document.getElementById('liveVoiceOverlay') && !document.getElementById('liveVoiceOverlay').classList.contains('hidden')) { | |
| showToast('Voice connection lost. Attempting to reconnect...', 'info'); | |
| initLiveSession(); // Try to re-init if overlay is visible | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Chat Error:", e); | |
| bubble.innerHTML = `<span style="color:#ff6b6b">Generation failed: ${e.message}</span>`; | |
| } finally { | |
| isStreaming = false; | |
| abortStream = false; | |
| showStopButton(false); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| if (sendBtn) { | |
| sendBtn.disabled = false; | |
| sendBtn.innerHTML = 'Send'; | |
| } | |
| } | |
| } | |
| function showStopButton(show) { | |
| const stopBtn = document.getElementById('stopBtn'); | |
| if (stopBtn) stopBtn.style.display = show ? 'inline-flex' : 'none'; | |
| } | |
| async function generateImageFromPrompt(prompt) { | |
| if (isStreaming) return; | |
| showToast('Generating image…', 'info'); | |
| try { | |
| const res = await fetch('/api/image', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success && data.image_url) { | |
| const badge = data.placeholder | |
| ? `\n\n> ⚠️ *Concept placeholder — AI image generation is unavailable on this host, so this is **not** a real AI image.*` | |
| : `\n\n> 🤖 *AI-generated image${data.provider ? ' (' + data.provider + ')' : ''}*`; | |
| addMsg('assistant', `🖼️ **${escHtml(prompt)}**\n\n${badge}`); | |
| showToast(data.placeholder ? 'Placeholder shown (no AI image)' : 'Image ready', data.placeholder ? 'info' : 'success'); | |
| } else { | |
| showToast('Image generation failed: ' + (data.error || 'unknown'), 'error'); | |
| } | |
| } catch (e) { | |
| showToast('Image generation failed: ' + e.message, 'error'); | |
| } | |
| } | |
| async function runCodeSnippet(code) { | |
| if (isStreaming) return; | |
| showToast('Running code…', 'info'); | |
| try { | |
| const res = await fetch('/api/execute/code', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ code, language: 'python' }) | |
| }); | |
| const data = await res.json(); | |
| const out = data.success ? data.output : (data.error || 'No output'); | |
| addMsg('assistant', '```python\n' + out + '\n```'); | |
| } catch (e) { | |
| showToast('Code execution failed: ' + e.message, 'error'); | |
| } | |
| } | |
| function renderToolCard(text) { | |
| // Extract the 🔧 processing line and render a styled tool-card | |
| const lines = text.split('\n'); | |
| const toolLine = lines.find(l => l.includes('🔧')); | |
| if (toolLine) { | |
| const label = toolLine.replace('🔧', '').replace('NeuralAI is processing tool:', '').trim(); | |
| return `<div class="tool-card"><span class="tool-icon">🔧</span><span class="tool-text">Processing tool: ${escHtml(label)}</span></div>` + fmt(lines.filter(l => l !== toolLine).join('\n')); | |
| } | |
| return fmt(text); | |
| } | |
| function updateModelStatus() { | |
| fetch('/api/health').then(r => r.json()).then(d => { | |
| const dot = document.getElementById('uplinkDot'); | |
| const label = document.getElementById('uplinkLabel'); | |
| const isLocal = d.llm_backend && d.llm_backend !== 'zo'; | |
| if (dot) dot.className = 'status-dot ' + (d.status === 'ready' ? 'online' : 'offline'); | |
| if (label) label.textContent = d.status === 'ready' ? (isLocal ? 'Ready (Local)' : 'Ready (External)') : (d.status || 'Offline'); | |
| }).catch(() => { | |
| const dot = document.getElementById('uplinkDot'); | |
| const label = document.getElementById('uplinkLabel'); | |
| if (dot) dot.className = 'status-dot offline'; | |
| if (label) label.textContent = 'Offline'; | |
| }); | |
| } | |
| function addMsg(role, content) { | |
| const container = document.getElementById('messages'); | |
| if (!container) return document.createElement('div'); | |
| const div = document.createElement('div'); | |
| div.className = `msg ${role === 'assistant' ? 'ai' : 'user'}`; | |
| const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| div.innerHTML = ` | |
| <div class="msg-meta"><span>${role === 'assistant' ? 'NeuralAI' : 'You'}</span><span class="msg-timestamp">${time}</span></div> | |
| ${role === 'assistant' ? '<div class="typing-label" style="display:none;font-size:12px;color:#8a8a9a;margin-bottom:4px;">NeuralAI is typing…</div>' : ''} | |
| <div class="msg-bubble">${content ? fmt(content) : ''}</div> | |
| `; | |
| container.appendChild(div); | |
| container.scrollTop = container.scrollHeight; | |
| return div; | |
| } | |
| // ======================================== | |
| // LIVE VOICE ENGINE (v2.0 - Multimodal Live) | |
| // ======================================== | |
| async function toggleLiveVoice(show) { | |
| const overlay = document.getElementById('liveVoiceOverlay'); | |
| const orb = overlay?.querySelector('.live-orb'); | |
| if (show) { | |
| overlay?.classList.remove('hidden'); | |
| try { | |
| await initLiveSession(); | |
| orb?.classList.add('listening'); | |
| } catch (err) { | |
| showToast('Live initialization failed: ' + err.message, 'error'); | |
| toggleLiveVoice(false); | |
| } | |
| } else { | |
| overlay?.classList.add('hidden'); | |
| orb?.classList.remove('listening', 'speaking'); | |
| stopLiveSession(); | |
| } | |
| } | |
| async function initLiveSession() { | |
| return new Promise(async (resolve, reject) => { | |
| try { | |
| if (voiceReconnectTimer) clearTimeout(voiceReconnectTimer); | |
| if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| if (audioCtx.state === 'suspended') { | |
| await audioCtx.resume(); | |
| } | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.hostname; | |
| // Voice WebSocket connects through the main service proxy at /voice/ws | |
| // This works universally: localhost, ZO Computer, Cloudflare, etc. | |
| const voiceOrigin = window.location.origin.replace('https:', 'wss:').replace('http:', 'ws:'); | |
| const voiceUrl = `${voiceOrigin}/voice/ws`; | |
| console.log(`[Voice] Connecting to: ${voiceUrl}`); | |
| showToast('Connecting to NeuralVoice Engine...', 'info'); | |
| if (voiceWS) { | |
| voiceWS.onclose = null; | |
| voiceWS.close(); | |
| } | |
| try { | |
| voiceWS = new WebSocket(voiceUrl); | |
| } catch (wsErr) { | |
| console.error("[Voice] WebSocket constructor failed:", wsErr); | |
| throw new Error("Failed to create connection: " + wsErr.message); | |
| } | |
| voiceWS.onopen = () => { | |
| console.log('[Voice] WebSocket Connected'); | |
| showToast('NeuralVoice Engine Online', 'success'); | |
| const selectedVoice = document.getElementById('voiceSelection')?.value || userSettings.neural_voice || 'Andrew'; | |
| voiceWS.send(JSON.stringify({ type: 'config', voice: selectedVoice })); | |
| startMicCapture().then(resolve).catch(reject); | |
| }; | |
| voiceWS.onmessage = async (event) => { | |
| console.log("[Voice] Message received:", event.data.substring(0, 100)); | |
| try { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'audio') { | |
| const orb = document.querySelector('.live-orb'); | |
| orb?.classList.remove('listening'); | |
| orb?.classList.add('speaking'); | |
| updateLiveStatus('NeuralAI Speaking...'); | |
| if (data.data) { | |
| const audioData = base64ToUint8Array(data.data); | |
| queueAudioChunk(audioData, data.sampleRate || 16000); | |
| } | |
| } else if (data.type === 'turn_complete') { | |
| const now = audioCtx.currentTime; | |
| const delay = Math.max(0, (nextPlayTime - now) * 1000); | |
| setTimeout(() => { | |
| const orb = document.querySelector('.live-orb'); | |
| if (orb && orb.classList.contains('speaking')) { | |
| orb.classList.remove('speaking', 'processing'); | |
| orb.classList.add('listening'); | |
| updateLiveStatus('NeuralAI Listening...'); | |
| } | |
| }, delay); | |
| } else if (data.type === 'error') { | |
| console.error('[Voice] Service Error:', data.message); | |
| showToast('Voice Error: ' + data.message, 'error'); | |
| } | |
| } catch (err) { | |
| console.error('[Voice] Handler Error:', err); | |
| } | |
| }; | |
| voiceWS.onclose = (e) => { | |
| console.log(`[Voice] WebSocket Closed (Code: ${e.code}, Reason: ${e.reason})`); | |
| voiceWS = null; | |
| if (e.code !== 1000 && document.getElementById('liveVoiceOverlay') && !document.getElementById('liveVoiceOverlay').classList.contains('hidden')) { | |
| showToast('Voice connection interrupted. Reconnecting...', 'warning'); | |
| voiceReconnectTimer = setTimeout(initLiveSession, 3000); | |
| } | |
| }; | |
| voiceWS.onerror = (err) => { | |
| console.error('[Voice] WebSocket Error:', err); | |
| showToast('NeuralVoice connection failed.', 'error'); | |
| }; | |
| } catch (err) { | |
| console.error("[Voice] Initialization failed:", err); | |
| reject(err); | |
| } | |
| }); | |
| } | |
| function base64ToUint8Array(base64) { | |
| const binaryString = atob(base64); | |
| const len = binaryString.length; | |
| const bytes = new Uint8Array(len); | |
| for (let i = 0; i < len; i++) { | |
| bytes[i] = binaryString.charCodeAt(i); | |
| } | |
| return bytes; | |
| } | |
| async function startMicCapture() { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SpeechRecognition) { | |
| showToast('Speech Recognition not supported in this browser.', 'error'); | |
| toggleLiveVoice(false); | |
| return; | |
| } | |
| if (!window.recognition) { | |
| console.log("[Voice] Initializing SpeechRecognition instance"); | |
| window.recognition = new SpeechRecognition(); | |
| window.recognition.continuous = true; | |
| window.recognition.interimResults = true; | |
| window.recognition.lang = 'en-US'; // Set explicit language | |
| window.recognition.onstart = () => { | |
| console.log('[Voice] STT Engine Started'); | |
| updateLiveStatus('NeuralAI Listening...'); | |
| showToast('Microphone Active - Start Speaking', 'success'); | |
| const orb = document.querySelector('.live-orb'); | |
| if (orb) orb.classList.add('listening'); | |
| }; | |
| window.recognition.onspeechstart = () => { | |
| console.log('[Voice] Speech detected'); | |
| updateLiveStatus('Hearing voice...'); | |
| }; | |
| window.recognition.onresult = (event) => { | |
| let finalTranscript = ''; | |
| for (let i = event.resultIndex; i < event.results.length; ++i) { | |
| if (event.results[i].isFinal) { | |
| finalTranscript += event.results[i][0].transcript; | |
| } else { | |
| const interim = event.results[i][0].transcript; | |
| console.log("[Voice] Interim transcript:", interim); | |
| updateLiveStatus('Hearing: ' + interim); | |
| } | |
| } | |
| if (finalTranscript.trim()) { | |
| const transcript = finalTranscript.trim(); | |
| console.log("[Voice] Final transcript:", transcript); | |
| updateLiveStatus('You: ' + transcript); | |
| sendMessage(transcript); | |
| const orb = document.querySelector('.live-orb'); | |
| orb?.classList.remove('listening'); | |
| orb?.classList.add('processing'); | |
| } | |
| }; | |
| window.recognition.onerror = (event) => { | |
| console.error("[Voice] STT Error:", event.error); | |
| if (event.error === 'not-allowed') { | |
| showToast('Microphone access denied. Check browser settings.', 'error'); | |
| toggleLiveVoice(false); | |
| } else if (event.error === 'network') { | |
| showToast('STT Network Error.', 'error'); | |
| } | |
| }; | |
| window.recognition.onend = () => { | |
| console.log("[Voice] STT Engine Ended"); | |
| const overlay = document.getElementById('liveVoiceOverlay'); | |
| if (overlay && !overlay.classList.contains('hidden')) { | |
| console.log('[Voice] Attempting auto-restart...'); | |
| setTimeout(() => { | |
| try { | |
| if (overlay && !overlay.classList.contains('hidden')) { | |
| window.recognition.start(); | |
| } | |
| } catch(e) { console.warn('[Voice] STT Restart failed:', e); } | |
| }, 500); | |
| } | |
| }; | |
| } | |
| return new Promise((resolve) => { | |
| try { | |
| window.recognition.stop(); | |
| } catch(e) {} | |
| setTimeout(() => { | |
| try { | |
| console.log("[Voice] Triggering STT start"); | |
| window.recognition.start(); | |
| resolve(); | |
| } catch(e) { | |
| console.warn('[Voice] Initial start failed:', e); | |
| resolve(); | |
| } | |
| }, 400); | |
| }); | |
| } | |
| function float32ToInt16(buffer) { | |
| let l = buffer.length; | |
| let buf = new Int16Array(l); | |
| while (l--) { | |
| let s = Math.max(-1, Math.min(1, buffer[l])); | |
| buf[l] = s < 0 ? s * 0x8000 : s * 0x7FFF; | |
| } | |
| return buf; | |
| } | |
| function queueAudioChunk(audioData, sampleRate) { | |
| audioQueue.push({ data: audioData, sampleRate: sampleRate }); | |
| if (!isPlaying) { | |
| playNextAudioChunk(); | |
| } | |
| } | |
| async function playNextAudioChunk() { | |
| if (audioQueue.length === 0) { | |
| isPlaying = false; | |
| return; | |
| } | |
| isPlaying = true; | |
| // Schedule all currently queued chunks | |
| while (audioQueue.length > 0) { | |
| const chunk = audioQueue.shift(); | |
| try { | |
| const data = chunk.data; | |
| const sampleRate = chunk.sampleRate || 16000; | |
| const buffer = data.buffer; | |
| const int16Data = new Int16Array(buffer, data.byteOffset, data.byteLength / 2); | |
| const float32Data = new Float32Array(int16Data.length); | |
| for (let i = 0; i < int16Data.length; i++) { | |
| float32Data[i] = int16Data[i] / 32768.0; | |
| } | |
| const audioBuffer = audioCtx.createBuffer(1, float32Data.length, sampleRate); | |
| audioBuffer.getChannelData(0).set(float32Data); | |
| const source = audioCtx.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| source.connect(audioCtx.destination); | |
| const now = audioCtx.currentTime; | |
| // Provide a small buffer ahead of current time if we fell behind | |
| if (nextPlayTime < now) nextPlayTime = now + 0.05; | |
| source.start(nextPlayTime); | |
| nextPlayTime += audioBuffer.duration; | |
| source.onended = () => { | |
| // If this was the last scheduled chunk and queue is empty, finish | |
| if (audioQueue.length === 0 && audioCtx.currentTime >= nextPlayTime - 0.1) { | |
| isPlaying = false; | |
| } else if (audioQueue.length > 0) { | |
| // If more chunks arrived while playing, schedule them | |
| playNextAudioChunk(); | |
| } | |
| }; | |
| } catch (err) { | |
| console.error('Playback Error:', err); | |
| } | |
| } | |
| } | |
| function stopLiveSession() { | |
| if (window.recognition) { | |
| window.recognition.stop(); | |
| } | |
| if (voiceWS) { | |
| voiceWS.close(); | |
| voiceWS = null; | |
| } | |
| if (micStream) { | |
| micStream.getTracks().forEach(t => t.stop()); | |
| micStream = null; | |
| } | |
| if (processor) { | |
| processor.disconnect(); | |
| processor = null; | |
| } | |
| } | |
| function updateLiveStatus(text) { | |
| const statusEl = document.getElementById('liveStatusText'); | |
| const transcriptEl = document.getElementById('liveTranscript'); | |
| if (text.startsWith('Hearing:') || text.startsWith('You:')) { | |
| if (transcriptEl) transcriptEl.innerText = text; | |
| } else { | |
| if (statusEl) statusEl.innerText = text; | |
| } | |
| } | |
| // ======================================== | |
| // SEARCH BAR & TABS | |
| // ======================================== | |
| function handleSearch() { | |
| const queryInput = document.getElementById("queryInput"); | |
| const text = queryInput?.value.trim(); | |
| if (!text) return; | |
| queryInput.value = ""; | |
| hideHistoryDropdown(); | |
| if (searchMode === "files") { | |
| switchTab('files'); | |
| const fileSearch = document.getElementById("fileSearch"); | |
| if (fileSearch) { | |
| fileSearch.value = text; | |
| filterFiles(); | |
| } | |
| } else if (searchMode === "system" || searchMode === "audit") { | |
| switchTab('terminal'); | |
| const termInput = document.getElementById("terminalInput"); | |
| if (termInput) { | |
| termInput.value = text; | |
| executeTerminalCmd(); | |
| } | |
| } else { | |
| switchTab('chat'); | |
| sendMessage(text); | |
| } | |
| } | |
| function showHistoryDropdown() { | |
| const dropdown = document.getElementById('historyDropdown'); | |
| if (dropdown) dropdown.style.display = 'block'; | |
| } | |
| function hideHistoryDropdown() { | |
| const dropdown = document.getElementById('historyDropdown'); | |
| if (dropdown) dropdown.style.display = 'none'; | |
| } | |
| function hideHistoryDropdownDelayed() { | |
| setTimeout(hideHistoryDropdown, 200); | |
| } | |
| // ======================================== | |
| // TERMINAL ENGINE | |
| // ======================================== | |
| async function initTerm() { | |
| if (termSid || isInitializingTerm) return; | |
| isInitializingTerm = true; | |
| try { | |
| // Clear ALL terminal outputs before connection message to prevent duplicates | |
| const el = document.getElementById("terminalOutput"); | |
| if (el) el.innerHTML = ''; | |
| const popEl = document.getElementById("popupTerminalOutput"); | |
| if (popEl) popEl.innerHTML = ''; | |
| const res = await fetch("/api/terminal/create", { method: "POST", headers: { "Authorization": `Bearer ${authToken}` } }); | |
| if (!res.ok) throw new Error(`Server responded ${res.status}`); | |
| const data = await res.json(); | |
| if (!data.session_id) throw new Error('No session ID returned'); | |
| termSid = data.session_id; | |
| termOut(`Connected to NeuralAI High-Velocity Console [SID: ${termSid}]`, 'info'); | |
| } catch (err) { termOut(`Failed to establish Neural Uplink: ${err.message}`, 'error'); } | |
| finally { isInitializingTerm = false; } | |
| } | |
| function toggleTerminalPopup() { | |
| const panel = document.getElementById('terminalPanel'); | |
| if (panel) { | |
| const isHidden = panel.classList.contains('hidden'); | |
| panel.classList.toggle('hidden'); | |
| if (isHidden) { | |
| initTerm(); | |
| setTimeout(() => document.getElementById('popupTerminalInput')?.focus(), 100); | |
| } | |
| } | |
| } | |
| async function executeTerminalCmd(isPopup = false) { | |
| const inputId = isPopup ? "popupTerminalInput" : "terminalInput"; | |
| const input = document.getElementById(inputId); | |
| const cmd = input?.value.trim(); | |
| if (!cmd || !termSid) return; | |
| termOut(`<span class="terminal-prompt">${currentShell === 'bash' ? 'root@Neural:~#' : (currentShell === 'python' ? '>>>' : 'node>')}</span> ${cmd}`, 'input'); | |
| input.value = ""; | |
| if (cmd.toLowerCase() === 'help') { | |
| termOut("NeuralAI Terminal Help:\n- bash: Default shell\n- python: Python runtime\n- node: JavaScript runtime\n- clear: Clear screen\n- restart: Reset session", 'info'); | |
| return; | |
| } | |
| try { | |
| const res = await fetch(`/api/terminal/${termSid}/send`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${authToken}` }, | |
| body: JSON.stringify({ command: cmd, shell: currentShell }) | |
| }); | |
| if (!res.ok) throw new Error(`Server responded ${res.status}`); | |
| const data = await res.json(); | |
| termOut(data.output || data.error || 'No output'); | |
| } catch (err) { termOut(`Terminal Error: ${err.message}`, 'error'); } | |
| } | |
| function termOut(txt, cls = '') { | |
| const targets = ["terminalOutput", "popupTerminalOutput"]; | |
| targets.forEach(id => { | |
| const el = document.getElementById(id); | |
| if (!el) return; | |
| const div = document.createElement('div'); | |
| div.className = `term-line ${cls}`; | |
| div.innerHTML = txt; | |
| el.appendChild(div); | |
| el.scrollTop = el.scrollHeight; | |
| }); | |
| } | |
| function clearTerm() { | |
| const el = document.getElementById("terminalOutput"); | |
| if (el) el.innerHTML = ''; | |
| const popEl = document.getElementById("popupTerminalOutput"); | |
| if (popEl) popEl.innerHTML = ''; | |
| } | |
| function restartTerm() { | |
| termSid = null; | |
| isInitializingTerm = false; | |
| clearTerm(); | |
| initTerm(); | |
| } | |
| function toggleHistory() { | |
| const panel = document.getElementById("historyPanel"); | |
| panel?.classList.toggle("hidden"); | |
| } | |
| function showSnippets() { | |
| document.getElementById("snippetsPanel")?.classList.remove("hidden"); | |
| } | |
| function hideSnippets() { | |
| document.getElementById("snippetsPanel")?.classList.add("hidden"); | |
| } | |
| function insertCmd(cmd) { | |
| const input = document.getElementById("terminalInput"); | |
| if (input) { | |
| input.value = cmd; | |
| input.focus(); | |
| } | |
| } | |
| // ==================== | |
| // NEURALDRIVE FILES | |
| // ==================== | |
| async function loadFiles() { | |
| const container = document.getElementById("filesGrid"); | |
| if (!container) return; | |
| container.innerHTML = '<div class="loading-shimmer">Scanning NeuralDrive...</div>'; | |
| try { | |
| const res = await fetch("/api/files", { headers: { "Authorization": `Bearer ${authToken}` } }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const data = await res.json(); | |
| // Backend returns either a raw array or {files: [...]} — handle both | |
| window.neuralFiles = Array.isArray(data) ? data : (data.files || []); | |
| renderFiles(window.neuralFiles); | |
| } catch (err) { | |
| console.error("NeuralDrive access error:", err); | |
| container.innerHTML = `<div class="error-state">Error accessing NeuralDrive: ${err.message}</div>`; | |
| } | |
| } | |
| function renderFiles(files) { | |
| const container = document.getElementById("filesGrid"); | |
| if (!container) return; | |
| if (!files || files.length === 0) { | |
| container.innerHTML = '<div class="empty-state">NeuralDrive is empty. Drag a file here or use Upload.</div>'; | |
| return; | |
| } | |
| container.innerHTML = files.map(f => { | |
| const isImage = f.type === 'image'; | |
| const icon = isImage ? '🖼️' : (f.type === 'dir' ? '📁' : '📄'); | |
| const thumb = isImage ? `<img class="file-thumb" src="/api/files/${encodeURIComponent(f.name)}" loading="lazy" alt="${escHtml(f.name)}">` : `<div class="file-icon">${icon}</div>`; | |
| const sizeStr = f.is_dir ? 'folder' : (f.size > 1048576 ? (f.size/1048576).toFixed(1)+' MB' : f.size > 1024 ? (f.size/1024).toFixed(1)+' KB' : f.size+' B'); | |
| return ` | |
| <div class="file-card" onclick="previewFile('${encodeURIComponent(f.name)}')"> | |
| <div class="file-preview">${thumb}</div> | |
| <div class="file-info"> | |
| <div class="file-name" title="${escHtml(f.name)}">${escHtml(f.name)}</div> | |
| <div class="file-meta">${sizeStr}</div> | |
| </div> | |
| <div class="file-actions"> | |
| <button class="file-action-btn" title="Download" onclick="event.stopPropagation(); window.open('/api/files/${encodeURIComponent(f.name)}', '_blank')">⬇️</button> | |
| <button class="file-action-btn" title="Delete" onclick="event.stopPropagation(); deleteFile('${encodeURIComponent(f.name)}')">🗑️</button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function filterFiles() { | |
| const query = document.getElementById("fileSearch")?.value.toLowerCase() || ""; | |
| const filtered = (window.neuralFiles || []).filter(f => f.name.toLowerCase().includes(query)); | |
| renderFiles(filtered); | |
| } | |
| function previewFile(filename) { | |
| const url = `/api/files/${filename}`; | |
| window.open(url, '_blank'); | |
| } | |
| async function deleteFile(filename) { | |
| if (!confirm(`Delete ${decodeURIComponent(filename)}?`)) return; | |
| try { | |
| const res = await fetch(`/api/files/${filename}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json().catch(() => ({})); | |
| if (data.success) { | |
| showToast('Deleted', 'success'); | |
| loadFiles(); | |
| } else { | |
| showToast('Delete failed: ' + (data.error || 'unknown'), 'error'); | |
| } | |
| } catch (e) { | |
| showToast('Delete failed: ' + e.message, 'error'); | |
| } | |
| } | |
| // Upload Logic | |
| function initUploadListeners() { | |
| const newFolderBtn = document.getElementById("newFolderBtn"); | |
| if (newFolderBtn) { | |
| newFolderBtn.addEventListener("click", async () => { | |
| const name = prompt("New folder name:"); | |
| if (!name || !name.trim()) return; | |
| try { | |
| const res = await fetch("/api/files/mkdir", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${authToken}` }, | |
| body: JSON.stringify({ name: name.trim() }) | |
| }); | |
| const data = await res.json().catch(() => ({})); | |
| if (data.success) { showToast("Folder created", "success"); loadFiles(); } | |
| else showToast("Failed: " + (data.error || "unknown"), "error"); | |
| } catch (e) { showToast("Failed: " + e.message, "error"); } | |
| }); | |
| } | |
| const uploadBtn = document.getElementById("uploadBtn"); | |
| const fileInput = document.getElementById("fileInputHidden"); | |
| const dropZone = document.getElementById("dropZone"); | |
| if (uploadBtn && fileInput) { | |
| uploadBtn.addEventListener("click", () => fileInput.click()); | |
| } | |
| if (fileInput) { | |
| fileInput.addEventListener("change", async (e) => { | |
| await handleFileUploads(e.target.files); | |
| fileInput.value = ""; // Reset | |
| }); | |
| } | |
| if (dropZone && fileInput) { | |
| dropZone.addEventListener("click", () => fileInput.click()); | |
| dropZone.addEventListener("dragover", (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = "#a78bfa"; | |
| dropZone.style.background = "rgba(167, 139, 250, 0.05)"; | |
| }); | |
| dropZone.addEventListener("dragleave", (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = "rgba(255, 255, 255, 0.1)"; | |
| dropZone.style.background = "transparent"; | |
| }); | |
| dropZone.addEventListener("drop", async (e) => { | |
| e.preventDefault(); | |
| dropZone.style.borderColor = "rgba(255, 255, 255, 0.1)"; | |
| dropZone.style.background = "transparent"; | |
| if (e.dataTransfer.files.length > 0) { | |
| await handleFileUploads(e.dataTransfer.files); | |
| } | |
| }); | |
| } | |
| } | |
| async function handleFileUploads(files) { | |
| if (!files || files.length === 0) return; | |
| for (let i = 0; i < files.length; i++) { | |
| const file = files[i]; | |
| const formData = new FormData(); | |
| formData.append("file", file); | |
| try { | |
| showToast(`Uploading ${file.name}...`, 'info'); | |
| const res = await fetch("/api/upload", { | |
| method: "POST", | |
| headers: { "Authorization": `Bearer ${authToken}` }, | |
| body: formData | |
| }); | |
| if (res.ok) { | |
| showToast(`${file.name} uploaded successfully`, 'success'); | |
| } else { | |
| const err = await res.json(); | |
| showToast(`Failed: ${err.error || 'Upload error'}`, 'error'); | |
| } | |
| } catch (e) { | |
| showToast(`Upload failed for ${file.name}`, 'error'); | |
| } | |
| } | |
| loadFiles(); // Refresh | |
| } | |
| // ==================== | |
| // SETTINGS & PERSISTENCE | |
| // ==================== | |
| async function loadUserProfile() { | |
| try { | |
| const res = await fetch('/api/user/me', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| if (data.user) { | |
| currentUser = data.user; | |
| if (document.getElementById('profileFirstName')) document.getElementById('profileFirstName').value = data.user.first_name || ''; | |
| if (document.getElementById('profileLastName')) document.getElementById('profileLastName').value = data.user.last_name || ''; | |
| if (document.getElementById('profileEmail')) document.getElementById('profileEmail').value = data.user.email || ''; | |
| if (document.getElementById('profileUsername')) document.getElementById('profileUsername').value = data.user.username || ''; | |
| if (document.getElementById('userBioInput')) document.getElementById('userBioInput').value = data.user.bio || ''; | |
| const initial = document.getElementById('profileInitial'); | |
| if (initial) initial.textContent = (data.user.username || 'U')[0].toUpperCase(); | |
| // Reveal founder-only self-update control | |
| if (data.user.is_founder && document.getElementById('selfUpdateArea')) { | |
| document.getElementById('selfUpdateArea').style.display = 'block'; | |
| } | |
| } | |
| loadBio(); | |
| loadMemoryList(); | |
| loadRulesList(); | |
| loadSettings(); | |
| } catch {} | |
| } | |
| async function saveFullProfile() { | |
| const data = { | |
| first_name: document.getElementById('profileFirstName')?.value, | |
| last_name: document.getElementById('profileLastName')?.value, | |
| email: document.getElementById('profileEmail')?.value, | |
| username: document.getElementById('profileUsername')?.value, | |
| bio: document.getElementById('userBioInput')?.value | |
| }; | |
| try { | |
| showToast('Updating Identity Vault...', 'info'); | |
| const res = await fetch('/api/user/update', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (res.ok) { | |
| showToast('Identity Vault Updated Successfully', 'success'); | |
| await initAuth(); // Refresh UI and greeting | |
| loadUserProfile(); // Reload profile fields | |
| } else { | |
| const err = await res.json(); | |
| throw new Error(err.error || 'Update failed'); | |
| } | |
| } catch (e) { | |
| showToast(`Vault Update Error: ${e.message}`, 'error'); | |
| } | |
| } | |
| async function loadSettings() { | |
| try { | |
| const res = await fetch('/api/settings', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| userSettings = data.settings || userSettings; | |
| if (userSettings.neural_voice) { | |
| const sel = document.getElementById('voiceSelection'); | |
| if (sel) sel.value = userSettings.neural_voice; | |
| } | |
| } catch {} | |
| updateArchitectureStatus(); | |
| } | |
| async function updateArchitectureStatus() { | |
| try { | |
| const res = await fetch('/api/status', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| const settingDevice = document.getElementById('settingDevice'); | |
| const versionNode = document.getElementById('settingVersion'); | |
| if (settingDevice) settingDevice.textContent = data.device || 'CPU (Optimized)'; | |
| if (versionNode) { | |
| versionNode.innerHTML = `<span style="color:#10b981; animation: pulse 2s infinite;">●</span> OTA Synced: ${data.version || 'v5.2.1'} (Rules: ${data.active_rules || 0})`; | |
| } | |
| } catch {} | |
| } | |
| // Add periodic updates for Architecture status | |
| setInterval(updateArchitectureStatus, 30000); | |
| async function saveVoicePreference() { | |
| const sel = document.getElementById('voiceSelection'); | |
| const voice = sel?.value; | |
| if (!voice) return; | |
| try { | |
| showToast(`Switching persona to ${voice}...`, 'info'); | |
| await fetch('/api/settings', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ neural_voice: voice }) | |
| }); | |
| // Update local state | |
| userSettings.neural_voice = voice; | |
| // If Live Session is active, send config update immediately | |
| if (voiceWS && voiceWS.readyState === WebSocket.OPEN) { | |
| console.log(`[Voice] Pushing live config update: ${voice}`); | |
| voiceWS.send(JSON.stringify({ type: 'config', voice: voice })); | |
| showToast(`${voice} Voice Active`, 'success'); | |
| } else { | |
| showToast('Persona saved. Start Live Mode to use.', 'success'); | |
| } | |
| } catch (err) { | |
| showToast('Failed to save voice preference.', 'error'); | |
| } | |
| } | |
| async function loadBio() { | |
| try { | |
| const res = await fetch('/api/settings', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| const input = document.getElementById('userBioInput'); | |
| if (input) input.value = data.settings?.user_bio || ''; | |
| } catch {} | |
| } | |
| // ======================================== | |
| // DEVELOPER / API ACCESS (BYO API) | |
| // ======================================== | |
| async function generateApiKey() { | |
| try { | |
| showToast('Generating API key...', 'info'); | |
| const res = await fetch('/api/settings/api-key', { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| const data = await res.json(); | |
| if (!data.success || !data.api_key) throw new Error(data.error || 'Failed to generate key'); | |
| document.getElementById('apiKeyField').value = data.api_key; | |
| document.getElementById('apiKeyResult').classList.remove('hidden'); | |
| document.getElementById('revokeApiKeyBtn').style.display = 'inline-block'; | |
| showToast('API key generated — copy it now (shown once)', 'success'); | |
| } catch (e) { | |
| showToast(`Key generation failed: ${e.message}`, 'error'); | |
| } | |
| } | |
| async function revokeApiKey() { | |
| try { | |
| const res = await fetch('/api/settings/api-key', { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| const data = await res.json(); | |
| if (!data.success) throw new Error(data.error || 'Revoke failed'); | |
| document.getElementById('apiKeyResult').classList.add('hidden'); | |
| document.getElementById('revokeApiKeyBtn').style.display = 'none'; | |
| showToast('API key revoked', 'success'); | |
| } catch (e) { | |
| showToast(`Revoke failed: ${e.message}`, 'error'); | |
| } | |
| } | |
| function copyApiKey() { | |
| const f = document.getElementById('apiKeyField'); | |
| if (f) { f.select(); navigator.clipboard.writeText(f.value); showToast('API key copied', 'success'); } | |
| } | |
| function copyApiEndpoint() { | |
| const f = document.getElementById('apiEndpointField'); | |
| if (f) { f.select(); navigator.clipboard.writeText(f.value); showToast('Endpoint copied', 'success'); } | |
| } | |
| async function selfUpdate() { | |
| if (!confirm('Pull latest code from GitHub and restart the service?')) return; | |
| try { | |
| showToast('Updating & restarting…', 'info'); | |
| const res = await fetch('/api/admin/update', { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (res.status === 403) { showToast('Founder access required', 'error'); return; } | |
| // On success the process re-execs; the connection drops. Reload after a beat. | |
| showToast('Update triggered — reloading…', 'success'); | |
| setTimeout(() => location.reload(), 2500); | |
| } catch { | |
| // Expected: the old process is gone after re-exec, so the fetch may abort. | |
| showToast('Update triggered — reloading…', 'success'); | |
| setTimeout(() => location.reload(), 2500); | |
| } | |
| } | |
| async function addMemoryFromTab() { | |
| const input = document.getElementById('memoryInput'); | |
| const fact = input?.value.trim(); | |
| if (!fact) return; | |
| try { | |
| showToast('Storing memory...', 'info'); | |
| const res = await fetch('/api/memory', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ fact }) | |
| }); | |
| if (!res.ok) throw new Error('Failed to save memory'); | |
| input.value = ''; | |
| await loadMemoryList(); | |
| showToast('Memory Cloud Updated', 'success'); | |
| } catch (err) { | |
| showToast('Error storing memory', 'error'); | |
| } | |
| } | |
| async function loadMemoryList() { | |
| const list = document.getElementById('memoryList'); | |
| if (!list) return; | |
| try { | |
| const res = await fetch('/api/memory', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| const items = data.facts || []; | |
| list.innerHTML = items.map(f => ` | |
| <div class="memory-item"> | |
| <span>${escHtml(f.fact)}</span> | |
| <button class="delete-btn" onclick="deleteMemoryItem(${f.id})" title="Purge Memory">×</button> | |
| </div> | |
| `).join('') || '<p style="font-size:12px;color:#888;padding:10px;">No dynamic memories stored.</p>'; | |
| } catch (err) { | |
| list.innerHTML = '<p style="color:#ff6b6b;padding:10px;">Failed to sync with Memory Cloud.</p>'; | |
| } | |
| } | |
| async function deleteMemoryItem(id) { | |
| if (!confirm('Purge this memory fact permanently?')) return; | |
| try { | |
| const res = await fetch(`/api/memory/${id}`, { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (!res.ok) throw new Error('Delete failed'); | |
| showToast('Memory Purged', 'success'); | |
| loadMemoryList(); | |
| } catch (err) { | |
| showToast('Failed to delete memory', 'error'); | |
| } | |
| } | |
| async function addRuleFromTab() { | |
| const input = document.getElementById('ruleInput'); | |
| const rule = input?.value.trim(); | |
| if (!rule) return; | |
| try { | |
| showToast('Deploying rule...', 'info'); | |
| const res = await fetch('/api/rules', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ rule }) | |
| }); | |
| if (!res.ok) throw new Error('Failed to save rule'); | |
| input.value = ''; | |
| await loadRulesList(); | |
| showToast('Behavioral Rule Active', 'success'); | |
| } catch (err) { | |
| showToast('Error saving rule', 'error'); | |
| } | |
| } | |
| async function loadRulesList() { | |
| const list = document.getElementById('rulesList'); | |
| if (!list) return; | |
| try { | |
| const res = await fetch('/api/rules', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| const data = await res.json(); | |
| const items = data.rules || []; | |
| list.innerHTML = items.map(r => ` | |
| <div class="rule-item"> | |
| <span>${escHtml(r.rule)}</span> | |
| <div class="rule-actions"> | |
| <button class="rule-toggle ${r.active ? 'on' : 'off'}" onclick="toggleRuleItem(${r.id})" title="Toggle Protocol"> | |
| ${r.active ? 'ON' : 'OFF'} | |
| </button> | |
| <button class="delete-btn" onclick="deleteRuleItem(${r.id})" title="Decommission Rule">×</button> | |
| </div> | |
| </div> | |
| `).join('') || '<p style="font-size:12px;color:#888;padding:10px;">No behavioral rules defined.</p>'; | |
| } catch (err) { | |
| list.innerHTML = '<p style="color:#ff6b6b;padding:10px;">Failed to sync Behavioral Rules.</p>'; | |
| } | |
| } | |
| async function toggleRuleItem(id) { | |
| try { | |
| const res = await fetch(`/api/rules/${id}/toggle`, { | |
| method: 'POST', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (!res.ok) throw new Error('Toggle failed'); | |
| loadRulesList(); | |
| } catch (err) { | |
| showToast('Failed to toggle rule', 'error'); | |
| } | |
| } | |
| async function deleteRuleItem(id) { | |
| if (!confirm('Decommission this behavioral protocol?')) return; | |
| try { | |
| const res = await fetch(`/api/rules/${id}`, { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${authToken}` } | |
| }); | |
| if (!res.ok) throw new Error('Delete failed'); | |
| showToast('Rule Decommissioned', 'success'); | |
| loadRulesList(); | |
| } catch (err) { | |
| showToast('Failed to delete rule', 'error'); | |
| } | |
| } | |
| // ======================================== | |
| // UX & UTILS | |
| // ======================================== | |
| function switchTab(tabName) { | |
| document.querySelectorAll('.nav-item').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tabName)); | |
| document.querySelectorAll('[id^="tab-"]').forEach(tab => tab.classList.add('hidden')); | |
| const activeTab = document.getElementById('tab-' + tabName); | |
| if (activeTab) activeTab.classList.remove('hidden'); | |
| if (tabName === 'terminal') initTerm(); | |
| if (tabName === 'files') loadFiles(); | |
| if (tabName === 'settings') loadUserProfile(); | |
| } | |
| function toggleDarkMode() { | |
| document.body.classList.toggle('dark-mode'); | |
| const isDark = document.body.classList.contains('dark-mode'); | |
| window.localStorage.setItem('neural_theme', isDark ? 'dark' : 'light'); | |
| } | |
| function showToast(msg, type = 'info') { | |
| const t = document.createElement('div'); | |
| t.className = `toast ${type}`; | |
| t.textContent = msg; | |
| document.body.appendChild(t); | |
| setTimeout(() => t.remove(), 4000); | |
| } | |
| async function openReleaseNotes() { | |
| const modal = document.getElementById('releaseNotesModal'); | |
| const body = document.getElementById('releaseNotesBody'); | |
| const title = document.getElementById('releaseNotesTitle'); | |
| if (!modal || !body) return; | |
| body.innerHTML = '<div class="rn-meta">Loading…</div>'; | |
| modal.classList.remove('hidden'); | |
| try { | |
| const res = await fetch('/api/release-notes', { headers: { 'Authorization': `Bearer ${authToken}` } }); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| title.textContent = data.title || '✨ What\'s New'; | |
| let html = ''; | |
| if (data.released) html += `<div class="rn-meta">Released ${escHtml(data.released)} · ${escHtml(data.version || '')}</div>`; | |
| (data.notes || []).forEach(n => { | |
| const tag = escHtml(n.tag || 'New'); | |
| const t = escHtml(n.title || ''); | |
| const b = escHtml(n.body || ''); | |
| html += `<div class="rn-item"><div class="rn-item-head"><span class="rn-tag ${tag}">${tag}</span><span class="rn-item-title">${t}</span></div><div class="rn-item-body">${b}</div></div>`; | |
| }); | |
| body.innerHTML = html || '<div class="rn-meta">No release notes available.</div>'; | |
| } catch (e) { | |
| body.innerHTML = '<div class="rn-meta">Failed to load release notes.</div>'; | |
| } | |
| } | |
| function escHtml(text) { | |
| if (!text) return ''; | |
| const p = document.createElement('p'); | |
| p.textContent = text; | |
| return p.innerHTML; | |
| } | |
| function fmt(text) { | |
| if (!text) return ''; | |
| // First, extract and render markdown images  as real <img> tags. | |
| // We build the HTML in a safe way: escape everything, then swap image tokens. | |
| const imagePlaceholders = []; | |
| let escaped = escHtml(text); | |
| // Find markdown image syntax in the ORIGINAL text (before escaping) to get raw URLs | |
| const imgRegex = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g; | |
| let m; | |
| while ((m = imgRegex.exec(text)) !== null) { | |
| const alt = m[1] || 'image'; | |
| const url = m[2]; | |
| // Only allow http(s) and same-origin relative paths (no javascript: etc.) | |
| if (/^(https?:\/\/|\/)/i.test(url)) { | |
| const token = `\u0000IMG${imagePlaceholders.length}\u0000`; | |
| imagePlaceholders.push(`<img class="gen-image" src="${escHtml(url)}" alt="${escHtml(alt)}" style="max-width:100%;border-radius:12px;margin:8px 0;">`); | |
| // Replace the markdown in the escaped string with the token | |
| const escapedMd = escHtml(m[0]); | |
| escaped = escaped.split(escapedMd).join(token); | |
| } | |
| } | |
| // Convert remaining markdown | |
| let out = escaped; | |
| out = out.replace(/\n/g, '<br>'); | |
| out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); | |
| out = out.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| // Restore image placeholders | |
| out = out.replace(/\u0000IMG(\d+)\u0000/g, (_, i) => imagePlaceholders[Number(i)] || ''); | |
| return out; | |
| } | |
| function closeOnboarding() { | |
| const overlay = document.getElementById("onboardingOverlay"); | |
| if (overlay) { | |
| overlay.classList.remove("visible"); | |
| window.localStorage.setItem("neural_onboarded", "true"); | |
| } | |
| } | |
| // ======================================== | |
| // INITIALIZATION | |
| // ======================================== | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log("NeuralAI Core UI Initializing..."); | |
| // Restore saved theme (default to dark to match the app's design language) | |
| try { | |
| const saved = window.localStorage.getItem('neural_theme'); | |
| if (saved === 'light') { | |
| document.body.classList.remove('dark-mode'); | |
| } else { | |
| document.body.classList.add('dark-mode'); | |
| } | |
| } catch (e) { document.body.classList.add('dark-mode'); } | |
| initAuth(); | |
| initUploadListeners(); | |
| // Wire Experimental Access Modes - Use addEventListener only | |
| document.getElementById('maestroSubmit')?.addEventListener('click', handleMaestroAuth); | |
| document.getElementById('guestSubmit')?.addEventListener('click', handleGuestAuth); | |
| // Wire top search bar (Google-style) — Ask button + Enter key | |
| document.getElementById('searchBtn')?.addEventListener('click', () => { | |
| console.log("Search Ask button clicked"); | |
| handleSearch(); | |
| }); | |
| document.getElementById('queryInput')?.addEventListener('keydown', e => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| handleSearch(); | |
| } | |
| }); | |
| // Wire bottom chat send button and enter key | |
| document.getElementById('sendBtn')?.addEventListener('click', () => { | |
| console.log("Send button clicked"); | |
| sendMessage(); | |
| }); | |
| document.getElementById('chatInput')?.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // Wire Voice Controls | |
| document.getElementById('voiceBtn')?.addEventListener('click', () => { | |
| console.log("Voice button clicked"); | |
| toggleLiveVoice(true); | |
| }); | |
| document.getElementById('closeLiveBtn')?.addEventListener('click', () => { | |
| console.log("Close voice button clicked"); | |
| toggleLiveVoice(false); | |
| }); | |
| // Wire Terminal Panel | |
| document.getElementById('popupTerminalRun')?.addEventListener('click', () => executeTerminalCmd(true)); | |
| document.getElementById('popupTerminalInput')?.addEventListener('keydown', e => { | |
| if (e.key === 'Enter') executeTerminalCmd(true); | |
| }); | |
| document.getElementById('closeTerminal')?.addEventListener('click', toggleTerminalPopup); | |
| // Wire search tabs properly to change mode AND execute if query exists | |
| document.querySelectorAll('.search-tabs .tab').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.search-tabs .tab').forEach(t => t.classList.remove('active')); | |
| btn.classList.add('active'); | |
| searchMode = btn.dataset.mode; | |
| // Also shift visual focus | |
| if (searchMode === 'files') { | |
| document.getElementById("queryInput").placeholder = "Search NeuralDrive files..."; | |
| } else if (searchMode === 'system') { | |
| document.getElementById("queryInput").placeholder = "Run system audit or terminal command..."; | |
| } else { | |
| document.getElementById("queryInput").placeholder = "Ask NeuralAI anything..."; | |
| } | |
| const text = document.getElementById("queryInput")?.value.trim(); | |
| if (text) { | |
| handleSearch(); | |
| } | |
| }); | |
| }); | |
| document.getElementById('authSubmit')?.addEventListener('click', handleAuth); | |
| document.getElementById('authToggle')?.addEventListener('click', toggleAuthMode); | |
| // Wire Experimental Access Modes | |
| document.getElementById('maestroSubmit')?.addEventListener('click', handleMaestroAuth); | |
| document.getElementById('guestSubmit')?.addEventListener('click', handleGuestAuth); | |
| document.getElementById('terminalInput')?.addEventListener('keydown', e => { | |
| if (e.key === 'Enter') executeTerminalCmd(); | |
| }); | |
| document.querySelectorAll('.terminal-tab').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.terminal-tab').forEach(t => t.classList.remove('active')); | |
| btn.classList.add('active'); | |
| currentShell = btn.dataset.shell; | |
| const prompt = document.getElementById("terminalPrompt"); | |
| if (prompt) prompt.textContent = currentShell === 'bash' ? 'root@Neural:~#' : (currentShell === 'python' ? '>>>' : 'node>'); | |
| }); | |
| }); | |
| document.querySelectorAll('.nav-item').forEach(btn => { | |
| btn.addEventListener('click', () => switchTab(btn.dataset.tab)); | |
| }); | |
| // Prompt Cards | |
| document.querySelectorAll('.prompt-card').forEach(card => { | |
| card.addEventListener('click', () => { | |
| sendMessage(card.dataset.prompt); | |
| }); | |
| }); | |
| // Stop button (appears during streaming) | |
| document.getElementById('stopBtn')?.addEventListener('click', () => { | |
| abortStream = true; | |
| }); | |
| // Code editor modal wiring (triggered on request, not a composer button) | |
| document.getElementById('closeCodeModal')?.addEventListener('click', () => { | |
| document.getElementById('codeModal')?.classList.add('hidden'); | |
| }); | |
| // Release Notes modal wiring | |
| document.getElementById('closeReleaseNotes')?.addEventListener('click', () => { | |
| document.getElementById('releaseNotesModal')?.classList.add('hidden'); | |
| }); | |
| document.getElementById('releaseNotesModal')?.addEventListener('click', (e) => { | |
| if (e.target.id === 'releaseNotesModal') e.target.classList.add('hidden'); | |
| }); | |
| document.getElementById('runCodeBtn')?.addEventListener('click', async () => { | |
| const editor = document.getElementById('codeEditor'); | |
| const out = document.getElementById('codeOutput'); | |
| if (!editor || !out) return; | |
| const code = editor.value; | |
| out.textContent = 'Running…'; | |
| try { | |
| const res = await fetch('/api/execute/code', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, | |
| body: JSON.stringify({ code, language: 'python' }) | |
| }); | |
| const data = await res.json(); | |
| out.textContent = data.success ? data.output : (data.error || 'No output'); | |
| // Also drop the result into the chat | |
| addMsg('assistant', '```python\n' + (data.success ? data.output : (data.error || 'No output')) + '\n```'); | |
| } catch (e) { | |
| out.textContent = 'Error: ' + e.message; | |
| } | |
| }); | |
| // Uplink toggle | |
| const uplinkToggle = document.getElementById('uplinkToggle'); | |
| if (uplinkToggle) { | |
| uplinkToggle.checked = uplinkEnabled; | |
| uplinkToggle.addEventListener('change', e => { | |
| uplinkEnabled = e.target.checked; | |
| window.localStorage.setItem('neural_uplink', uplinkEnabled ? '1' : '0'); | |
| }); | |
| } | |
| const savedUplink = window.localStorage.getItem('neural_uplink'); | |
| if (savedUplink !== null) uplinkEnabled = savedUplink === '1'; | |
| // Model status polling | |
| updateModelStatus(); | |
| setInterval(updateModelStatus, 30000); | |
| }); | |
| // Global Exports | |
| window.switchTab = switchTab; | |
| window.loadConversation = loadConversation; | |
| window.deleteConversation = deleteConversation; | |
| window.renameConversation = renameConversation; | |
| window.logout = logout; | |
| window.toggleDarkMode = toggleDarkMode; | |
| window.saveFullProfile = saveFullProfile; | |
| window.saveVoicePreference = saveVoicePreference; | |
| window.addMemoryFromTab = addMemoryFromTab; | |
| window.deleteMemoryItem = deleteMemoryItem; | |
| window.addRuleFromTab = addRuleFromTab; | |
| window.deleteRuleItem = deleteRuleItem; | |
| window.toggleRuleItem = toggleRuleItem; | |
| window.previewFile = previewFile; | |
| window.clearTerm = clearTerm; | |
| window.restartTerm = restartTerm; | |
| window.toggleHistory = toggleHistory; | |
| window.showSnippets = showSnippets; | |
| window.hideSnippets = hideSnippets; | |
| window.insertCmd = insertCmd; | |
| window.showHistoryDropdown = showHistoryDropdown; | |
| window.hideHistoryDropdownDelayed = hideHistoryDropdownDelayed; | |
| window.filterFiles = filterFiles; | |
| window.handleSearch = handleSearch; | |
| window.sendMessage = sendMessage; | |
| window.toggleLiveVoice = toggleLiveVoice; | |
| window.closeOnboarding = closeOnboarding; | |
| window.toggleTerminalPopup = toggleTerminalPopup; | |
| window.executeTerminalCmd = executeTerminalCmd; | |