Subject-Emu-5259 commited on
Commit
80bdf6c
·
verified ·
1 Parent(s): e317252

sync: update from-scratch/web_ui/static/js/main_v2.js

Browse files
from-scratch/web_ui/static/js/main_v2.js CHANGED
@@ -10,9 +10,11 @@ let authToken = window.localStorage.getItem("neural_token");
10
  let currentUser = null;
11
  let currentConversationId = null;
12
  let isStreaming = false;
 
 
13
  let conversation = [];
14
  let attachedFiles = {};
15
- let uplinkEnabled = true;
16
  let termSid = null;
17
  let termPoll = null;
18
  let authMode = "login";
@@ -283,6 +285,7 @@ async function createNewConversation() {
283
 
284
  async function loadConversation(id) {
285
  currentConversationId = id;
 
286
  try {
287
  const res = await fetch(`/api/conversations/${id}`, {
288
  headers: { 'Authorization': `Bearer ${authToken}` }
@@ -333,16 +336,22 @@ async function renameConversation(id) {
333
  function renderConversationList(convs) {
334
  const list = document.getElementById('sidebarHistoryList');
335
  if (!list) return;
336
- list.innerHTML = convs.map(c => `
 
 
337
  <div class="history-item ${currentConversationId === c.id ? 'active' : ''}" onclick="loadConversation('${c.id}')">
338
  <span class="history-item-text">${escHtml(c.title)}</span>
 
339
  <div class="history-item-actions">
340
  <button class="history-item-rename" onclick="event.stopPropagation(); renameConversation('${c.id}')">✏️</button>
341
  <button class="history-item-delete" onclick="event.stopPropagation(); deleteConversation('${c.id}')">&times;</button>
342
  </div>
343
  </div>
344
- `).join('') || '<p style="font-size:12px;color:#555;padding:10px;">No logs found.</p>';
345
-
 
 
 
346
  // Update dropdown too
347
  const dropList = document.getElementById('historyDropdownList');
348
  if (dropList) {
@@ -365,6 +374,33 @@ async function sendMessage(textOverride = null) {
365
  console.warn("SendMessage: No text to send");
366
  return;
367
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
  if (isStreaming) {
370
  console.warn("SendMessage: Already streaming");
@@ -403,12 +439,16 @@ async function sendMessage(textOverride = null) {
403
  const assistantMsg = addMsg('assistant', '');
404
  const bubble = assistantMsg.querySelector('.msg-bubble');
405
  bubble.innerHTML = '<div class="thinking-dots"><span></span><span></span><span></span></div>';
 
 
 
 
406
 
407
  try {
408
  const res = await fetch('/api/chat', {
409
  method: 'POST',
410
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
411
- body: JSON.stringify({ prompt: text, conversation_id: currentConversationId, messages: conversation })
412
  });
413
 
414
  if (!res.ok) {
@@ -419,9 +459,18 @@ async function sendMessage(textOverride = null) {
419
  const reader = res.body.getReader();
420
  const decoder = new TextDecoder();
421
  let full = '';
 
422
  bubble.innerHTML = '';
423
 
424
  while (true) {
 
 
 
 
 
 
 
 
425
  const { done, value } = await reader.read();
426
  if (done) break;
427
  const chunk = decoder.decode(value);
@@ -433,8 +482,17 @@ async function sendMessage(textOverride = null) {
433
  try {
434
  const data = JSON.parse(raw);
435
  if (data.content) {
 
 
 
 
436
  full += data.content;
437
- bubble.innerHTML = fmt(full);
 
 
 
 
 
438
  const msgs = document.getElementById('messages');
439
  msgs.scrollTop = msgs.scrollHeight;
440
  }
@@ -466,6 +524,8 @@ async function sendMessage(textOverride = null) {
466
  bubble.innerHTML = `<span style="color:#ff6b6b">Generation failed: ${e.message}</span>`;
467
  } finally {
468
  isStreaming = false;
 
 
469
  const sendBtn = document.getElementById('sendBtn');
470
  if (sendBtn) {
471
  sendBtn.disabled = false;
@@ -474,6 +534,78 @@ async function sendMessage(textOverride = null) {
474
  }
475
  }
476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  function addMsg(role, content) {
478
  const container = document.getElementById('messages');
479
  if (!container) return document.createElement('div');
@@ -482,6 +614,7 @@ function addMsg(role, content) {
482
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
483
  div.innerHTML = `
484
  <div class="msg-meta"><span>${role === 'assistant' ? 'NeuralAI' : 'You'}</span><span class="msg-timestamp">${time}</span></div>
 
485
  <div class="msg-bubble">${content ? fmt(content) : ''}</div>
486
  `;
487
  container.appendChild(div);
@@ -525,19 +658,12 @@ async function initLiveSession() {
525
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
526
  const host = window.location.hostname;
527
 
528
- // Robust Voice Host Detection
529
- let voiceHost = host.replace('neuralai-', 'neural-voice-');
530
- if (host === 'localhost' || host === '127.0.0.1') {
531
- voiceHost = `${host}:5001`;
532
- } else if (!host.includes('neural-voice-')) {
533
- const parts = host.split('.');
534
- if (parts.length >= 3) {
535
- const handle = parts[0].replace('neuralai-', '');
536
- voiceHost = `neural-voice-${handle}.zocomputer.io`;
537
- }
538
- }
539
-
540
- console.log(`[Voice] Connecting to: ${voiceHost}`);
541
  showToast('Connecting to NeuralVoice Engine...', 'info');
542
 
543
  if (voiceWS) {
@@ -546,7 +672,7 @@ async function initLiveSession() {
546
  }
547
 
548
  try {
549
- voiceWS = new WebSocket(`${protocol}//${voiceHost}/ws`);
550
  } catch (wsErr) {
551
  console.error("[Voice] WebSocket constructor failed:", wsErr);
552
  throw new Error("Failed to create connection: " + wsErr.message);
@@ -983,7 +1109,8 @@ async function loadFiles() {
983
  const res = await fetch("/api/files", { headers: { "Authorization": `Bearer ${authToken}` } });
984
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
985
  const data = await res.json();
986
- window.neuralFiles = data.files || [];
 
987
  renderFiles(window.neuralFiles);
988
  } catch (err) {
989
  console.error("NeuralDrive access error:", err);
@@ -995,20 +1122,28 @@ function renderFiles(files) {
995
  const container = document.getElementById("filesGrid");
996
  if (!container) return;
997
  if (!files || files.length === 0) {
998
- container.innerHTML = '<div class="empty-state">NeuralDrive is empty.</div>';
999
  return;
1000
  }
1001
- container.innerHTML = files.map(f => `
1002
- <div class="file-card" onclick="previewFile('${f.type}', '${f.name}')">
 
 
 
 
 
 
1003
  <div class="file-info">
1004
- <div class="file-icon">${f.name.endsWith('.png') || f.name.endsWith('.jpg') ? '🖼️' : '📄'}</div>
1005
- <div class="file-name">${f.name}</div>
1006
  </div>
1007
  <div class="file-actions">
1008
- <button class="file-action-btn" onclick="event.stopPropagation(); window.open('/api/files/${f.type}/${f.name}', '_blank')">⬇️</button>
 
1009
  </div>
1010
  </div>
1011
- `).join('');
 
1012
  }
1013
 
1014
  function filterFiles() {
@@ -1017,13 +1152,47 @@ function filterFiles() {
1017
  renderFiles(filtered);
1018
  }
1019
 
1020
- function previewFile(folder, filename) {
1021
- const url = `/api/files/${folder}/${filename}`;
1022
  window.open(url, '_blank');
1023
  }
1024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1025
  // Upload Logic
1026
  function initUploadListeners() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1027
  const uploadBtn = document.getElementById("uploadBtn");
1028
  const fileInput = document.getElementById("fileInputHidden");
1029
  const dropZone = document.getElementById("dropZone");
@@ -1104,6 +1273,10 @@ async function loadUserProfile() {
1104
  if (document.getElementById('userBioInput')) document.getElementById('userBioInput').value = data.user.bio || '';
1105
  const initial = document.getElementById('profileInitial');
1106
  if (initial) initial.textContent = (data.user.username || 'U')[0].toUpperCase();
 
 
 
 
1107
  }
1108
  loadBio();
1109
  loadMemoryList();
@@ -1208,6 +1381,72 @@ async function loadBio() {
1208
  } catch {}
1209
  }
1210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  async function addMemoryFromTab() {
1212
  const input = document.getElementById('memoryInput');
1213
  const fact = input?.value.trim();
@@ -1368,6 +1607,32 @@ function showToast(msg, type = 'info') {
1368
  setTimeout(() => t.remove(), 4000);
1369
  }
1370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1371
  function escHtml(text) {
1372
  if (!text) return '';
1373
  const p = document.createElement('p');
@@ -1376,10 +1641,33 @@ function escHtml(text) {
1376
  }
1377
 
1378
  function fmt(text) {
1379
- let out = escHtml(text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1380
  out = out.replace(/\n/g, '<br>');
1381
  out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1382
  out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
 
 
1383
  return out;
1384
  }
1385
 
@@ -1396,6 +1684,15 @@ function closeOnboarding() {
1396
  // ========================================
1397
  document.addEventListener('DOMContentLoaded', () => {
1398
  console.log("NeuralAI Core UI Initializing...");
 
 
 
 
 
 
 
 
 
1399
  initAuth();
1400
  initUploadListeners();
1401
 
@@ -1403,7 +1700,19 @@ document.addEventListener('DOMContentLoaded', () => {
1403
  document.getElementById('maestroSubmit')?.addEventListener('click', handleMaestroAuth);
1404
  document.getElementById('guestSubmit')?.addEventListener('click', handleGuestAuth);
1405
 
1406
- // Wire chat send button and enter key
 
 
 
 
 
 
 
 
 
 
 
 
1407
  document.getElementById('sendBtn')?.addEventListener('click', () => {
1408
  console.log("Send button clicked");
1409
  sendMessage();
@@ -1485,6 +1794,60 @@ document.addEventListener('DOMContentLoaded', () => {
1485
  sendMessage(card.dataset.prompt);
1486
  });
1487
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1488
  });
1489
 
1490
  // Global Exports
 
10
  let currentUser = null;
11
  let currentConversationId = null;
12
  let isStreaming = false;
13
+ let abortStream = false;
14
+ let lastSeen = {};
15
  let conversation = [];
16
  let attachedFiles = {};
17
+ let uplinkEnabled = false;
18
  let termSid = null;
19
  let termPoll = null;
20
  let authMode = "login";
 
285
 
286
  async function loadConversation(id) {
287
  currentConversationId = id;
288
+ lastSeen[id] = new Date().toISOString();
289
  try {
290
  const res = await fetch(`/api/conversations/${id}`, {
291
  headers: { 'Authorization': `Bearer ${authToken}` }
 
336
  function renderConversationList(convs) {
337
  const list = document.getElementById('sidebarHistoryList');
338
  if (!list) return;
339
+ list.innerHTML = convs.map(c => {
340
+ const isUnread = c.id !== currentConversationId && c.updated_at && lastSeen[c.id] && c.updated_at > lastSeen[c.id];
341
+ return `
342
  <div class="history-item ${currentConversationId === c.id ? 'active' : ''}" onclick="loadConversation('${c.id}')">
343
  <span class="history-item-text">${escHtml(c.title)}</span>
344
+ ${isUnread ? '<span class="unread-badge">NEW</span>' : ''}
345
  <div class="history-item-actions">
346
  <button class="history-item-rename" onclick="event.stopPropagation(); renameConversation('${c.id}')">✏️</button>
347
  <button class="history-item-delete" onclick="event.stopPropagation(); deleteConversation('${c.id}')">&times;</button>
348
  </div>
349
  </div>
350
+ `}).join('') || '<p style="font-size:12px;color:#555;padding:10px;">No logs found.</p>';
351
+
352
+ // Track last-seen timestamps for unread detection
353
+ convs.forEach(c => { if (!lastSeen[c.id]) lastSeen[c.id] = c.updated_at; });
354
+
355
  // Update dropdown too
356
  const dropList = document.getElementById('historyDropdownList');
357
  if (dropList) {
 
374
  console.warn("SendMessage: No text to send");
375
  return;
376
  }
377
+
378
+ // Intent detection: trigger image/code "upon request" instead of a composer button
379
+ const lower = text.toLowerCase();
380
+ // Image intent: a generation verb + a subject, OR any explicit image noun.
381
+ // Covers "generate a dog", "make a cat", "draw a sunset", "image of a car", etc.
382
+ const imageVerb = /(generat|creat|make|draw|render|paint|produce|show me|give me|design)/.test(lower);
383
+ const imageNoun = /\b(image|picture|photo|drawing|render|painting|pic|gif|meme)\b/.test(lower);
384
+ const imageIntent = (imageVerb && lower.length > 3) || imageNoun;
385
+ 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);
386
+ if (imageIntent && !lower.includes('chat') && !lower.includes('explain')) {
387
+ if (!textOverride && input) input.value = '';
388
+ generateImageFromPrompt(text);
389
+ return;
390
+ }
391
+ if (codeIntent) {
392
+ const modal = document.getElementById('codeModal');
393
+ const editor = document.getElementById('codeEditor');
394
+ if (modal && editor) {
395
+ // Extract a fenced code block if present, else use the whole prompt
396
+ const fence = text.match(/```(?:python)?\s*([\s\S]*?)```/i);
397
+ editor.value = fence ? fence[1].trim() : text.replace(/run (this )?code:?/i, '').trim();
398
+ modal.classList.remove('hidden');
399
+ editor.focus();
400
+ if (!textOverride && input) input.value = '';
401
+ return;
402
+ }
403
+ }
404
 
405
  if (isStreaming) {
406
  console.warn("SendMessage: Already streaming");
 
439
  const assistantMsg = addMsg('assistant', '');
440
  const bubble = assistantMsg.querySelector('.msg-bubble');
441
  bubble.innerHTML = '<div class="thinking-dots"><span></span><span></span><span></span></div>';
442
+ const typingLabel = assistantMsg.querySelector('.typing-label');
443
+ if (typingLabel) typingLabel.style.display = 'block';
444
+ abortStream = false;
445
+ showStopButton(true);
446
 
447
  try {
448
  const res = await fetch('/api/chat', {
449
  method: 'POST',
450
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
451
+ body: JSON.stringify({ prompt: text, conversation_id: currentConversationId, messages: conversation, use_uplink: uplinkEnabled })
452
  });
453
 
454
  if (!res.ok) {
 
459
  const reader = res.body.getReader();
460
  const decoder = new TextDecoder();
461
  let full = '';
462
+ let firstToken = true;
463
  bubble.innerHTML = '';
464
 
465
  while (true) {
466
+ if (abortStream) {
467
+ await fetch('/api/chat/stop', {
468
+ method: 'POST',
469
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
470
+ body: JSON.stringify({ conversation_id: currentConversationId })
471
+ }).catch(() => {});
472
+ break;
473
+ }
474
  const { done, value } = await reader.read();
475
  if (done) break;
476
  const chunk = decoder.decode(value);
 
482
  try {
483
  const data = JSON.parse(raw);
484
  if (data.content) {
485
+ if (firstToken) {
486
+ firstToken = false;
487
+ if (typingLabel) typingLabel.style.display = 'none';
488
+ }
489
  full += data.content;
490
+ // Render tool-cards when an uplink agent emits a 🔧 marker
491
+ if (data.content.includes('🔧') || full.includes('🔧 NeuralAI is processing tool')) {
492
+ bubble.innerHTML = renderToolCard(full);
493
+ } else {
494
+ bubble.innerHTML = fmt(full);
495
+ }
496
  const msgs = document.getElementById('messages');
497
  msgs.scrollTop = msgs.scrollHeight;
498
  }
 
524
  bubble.innerHTML = `<span style="color:#ff6b6b">Generation failed: ${e.message}</span>`;
525
  } finally {
526
  isStreaming = false;
527
+ abortStream = false;
528
+ showStopButton(false);
529
  const sendBtn = document.getElementById('sendBtn');
530
  if (sendBtn) {
531
  sendBtn.disabled = false;
 
534
  }
535
  }
536
 
537
+ function showStopButton(show) {
538
+ const stopBtn = document.getElementById('stopBtn');
539
+ if (stopBtn) stopBtn.style.display = show ? 'inline-flex' : 'none';
540
+ }
541
+
542
+ async function generateImageFromPrompt(prompt) {
543
+ if (isStreaming) return;
544
+ showToast('Generating image…', 'info');
545
+ try {
546
+ const res = await fetch('/api/image', {
547
+ method: 'POST',
548
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
549
+ body: JSON.stringify({ prompt })
550
+ });
551
+ const data = await res.json();
552
+ if (data.success && data.image_url) {
553
+ const badge = data.placeholder
554
+ ? `\n\n> ⚠️ *Concept placeholder — AI image generation is unavailable on this host, so this is **not** a real AI image.*`
555
+ : `\n\n> 🤖 *AI-generated image${data.provider ? ' (' + data.provider + ')' : ''}*`;
556
+ addMsg('assistant', `🖼️ **${escHtml(prompt)}**\n\n![generated](${data.image_url})${badge}`);
557
+ showToast(data.placeholder ? 'Placeholder shown (no AI image)' : 'Image ready', data.placeholder ? 'info' : 'success');
558
+ } else {
559
+ showToast('Image generation failed: ' + (data.error || 'unknown'), 'error');
560
+ }
561
+ } catch (e) {
562
+ showToast('Image generation failed: ' + e.message, 'error');
563
+ }
564
+ }
565
+
566
+ async function runCodeSnippet(code) {
567
+ if (isStreaming) return;
568
+ showToast('Running code…', 'info');
569
+ try {
570
+ const res = await fetch('/api/execute/code', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
573
+ body: JSON.stringify({ code, language: 'python' })
574
+ });
575
+ const data = await res.json();
576
+ const out = data.success ? data.output : (data.error || 'No output');
577
+ addMsg('assistant', '```python\n' + out + '\n```');
578
+ } catch (e) {
579
+ showToast('Code execution failed: ' + e.message, 'error');
580
+ }
581
+ }
582
+
583
+ function renderToolCard(text) {
584
+ // Extract the 🔧 processing line and render a styled tool-card
585
+ const lines = text.split('\n');
586
+ const toolLine = lines.find(l => l.includes('🔧'));
587
+ if (toolLine) {
588
+ const label = toolLine.replace('🔧', '').replace('NeuralAI is processing tool:', '').trim();
589
+ 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'));
590
+ }
591
+ return fmt(text);
592
+ }
593
+
594
+ function updateModelStatus() {
595
+ fetch('/api/health').then(r => r.json()).then(d => {
596
+ const dot = document.getElementById('uplinkDot');
597
+ const label = document.getElementById('uplinkLabel');
598
+ const isLocal = d.llm_backend && d.llm_backend !== 'zo';
599
+ if (dot) dot.className = 'status-dot ' + (d.status === 'ready' ? 'online' : 'offline');
600
+ if (label) label.textContent = d.status === 'ready' ? (isLocal ? 'Ready (Local)' : 'Ready (External)') : (d.status || 'Offline');
601
+ }).catch(() => {
602
+ const dot = document.getElementById('uplinkDot');
603
+ const label = document.getElementById('uplinkLabel');
604
+ if (dot) dot.className = 'status-dot offline';
605
+ if (label) label.textContent = 'Offline';
606
+ });
607
+ }
608
+
609
  function addMsg(role, content) {
610
  const container = document.getElementById('messages');
611
  if (!container) return document.createElement('div');
 
614
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
615
  div.innerHTML = `
616
  <div class="msg-meta"><span>${role === 'assistant' ? 'NeuralAI' : 'You'}</span><span class="msg-timestamp">${time}</span></div>
617
+ ${role === 'assistant' ? '<div class="typing-label" style="display:none;font-size:12px;color:#8a8a9a;margin-bottom:4px;">NeuralAI is typing…</div>' : ''}
618
  <div class="msg-bubble">${content ? fmt(content) : ''}</div>
619
  `;
620
  container.appendChild(div);
 
658
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
659
  const host = window.location.hostname;
660
 
661
+ // Voice WebSocket connects through the main service proxy at /voice/ws
662
+ // This works universally: localhost, ZO Computer, Cloudflare, etc.
663
+ const voiceOrigin = window.location.origin.replace('https:', 'wss:').replace('http:', 'ws:');
664
+ const voiceUrl = `${voiceOrigin}/voice/ws`;
665
+
666
+ console.log(`[Voice] Connecting to: ${voiceUrl}`);
 
 
 
 
 
 
 
667
  showToast('Connecting to NeuralVoice Engine...', 'info');
668
 
669
  if (voiceWS) {
 
672
  }
673
 
674
  try {
675
+ voiceWS = new WebSocket(voiceUrl);
676
  } catch (wsErr) {
677
  console.error("[Voice] WebSocket constructor failed:", wsErr);
678
  throw new Error("Failed to create connection: " + wsErr.message);
 
1109
  const res = await fetch("/api/files", { headers: { "Authorization": `Bearer ${authToken}` } });
1110
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1111
  const data = await res.json();
1112
+ // Backend returns either a raw array or {files: [...]} — handle both
1113
+ window.neuralFiles = Array.isArray(data) ? data : (data.files || []);
1114
  renderFiles(window.neuralFiles);
1115
  } catch (err) {
1116
  console.error("NeuralDrive access error:", err);
 
1122
  const container = document.getElementById("filesGrid");
1123
  if (!container) return;
1124
  if (!files || files.length === 0) {
1125
+ container.innerHTML = '<div class="empty-state">NeuralDrive is empty. Drag a file here or use Upload.</div>';
1126
  return;
1127
  }
1128
+ container.innerHTML = files.map(f => {
1129
+ const isImage = f.type === 'image';
1130
+ const icon = isImage ? '🖼️' : (f.type === 'dir' ? '📁' : '📄');
1131
+ 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>`;
1132
+ 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');
1133
+ return `
1134
+ <div class="file-card" onclick="previewFile('${encodeURIComponent(f.name)}')">
1135
+ <div class="file-preview">${thumb}</div>
1136
  <div class="file-info">
1137
+ <div class="file-name" title="${escHtml(f.name)}">${escHtml(f.name)}</div>
1138
+ <div class="file-meta">${sizeStr}</div>
1139
  </div>
1140
  <div class="file-actions">
1141
+ <button class="file-action-btn" title="Download" onclick="event.stopPropagation(); window.open('/api/files/${encodeURIComponent(f.name)}', '_blank')">⬇️</button>
1142
+ <button class="file-action-btn" title="Delete" onclick="event.stopPropagation(); deleteFile('${encodeURIComponent(f.name)}')">🗑️</button>
1143
  </div>
1144
  </div>
1145
+ `;
1146
+ }).join('');
1147
  }
1148
 
1149
  function filterFiles() {
 
1152
  renderFiles(filtered);
1153
  }
1154
 
1155
+ function previewFile(filename) {
1156
+ const url = `/api/files/${filename}`;
1157
  window.open(url, '_blank');
1158
  }
1159
 
1160
+ async function deleteFile(filename) {
1161
+ if (!confirm(`Delete ${decodeURIComponent(filename)}?`)) return;
1162
+ try {
1163
+ const res = await fetch(`/api/files/${filename}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}` } });
1164
+ const data = await res.json().catch(() => ({}));
1165
+ if (data.success) {
1166
+ showToast('Deleted', 'success');
1167
+ loadFiles();
1168
+ } else {
1169
+ showToast('Delete failed: ' + (data.error || 'unknown'), 'error');
1170
+ }
1171
+ } catch (e) {
1172
+ showToast('Delete failed: ' + e.message, 'error');
1173
+ }
1174
+ }
1175
+
1176
  // Upload Logic
1177
  function initUploadListeners() {
1178
+ const newFolderBtn = document.getElementById("newFolderBtn");
1179
+ if (newFolderBtn) {
1180
+ newFolderBtn.addEventListener("click", async () => {
1181
+ const name = prompt("New folder name:");
1182
+ if (!name || !name.trim()) return;
1183
+ try {
1184
+ const res = await fetch("/api/files/mkdir", {
1185
+ method: "POST",
1186
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${authToken}` },
1187
+ body: JSON.stringify({ name: name.trim() })
1188
+ });
1189
+ const data = await res.json().catch(() => ({}));
1190
+ if (data.success) { showToast("Folder created", "success"); loadFiles(); }
1191
+ else showToast("Failed: " + (data.error || "unknown"), "error");
1192
+ } catch (e) { showToast("Failed: " + e.message, "error"); }
1193
+ });
1194
+ }
1195
+
1196
  const uploadBtn = document.getElementById("uploadBtn");
1197
  const fileInput = document.getElementById("fileInputHidden");
1198
  const dropZone = document.getElementById("dropZone");
 
1273
  if (document.getElementById('userBioInput')) document.getElementById('userBioInput').value = data.user.bio || '';
1274
  const initial = document.getElementById('profileInitial');
1275
  if (initial) initial.textContent = (data.user.username || 'U')[0].toUpperCase();
1276
+ // Reveal founder-only self-update control
1277
+ if (data.user.is_founder && document.getElementById('selfUpdateArea')) {
1278
+ document.getElementById('selfUpdateArea').style.display = 'block';
1279
+ }
1280
  }
1281
  loadBio();
1282
  loadMemoryList();
 
1381
  } catch {}
1382
  }
1383
 
1384
+ // ========================================
1385
+ // DEVELOPER / API ACCESS (BYO API)
1386
+ // ========================================
1387
+ async function generateApiKey() {
1388
+ try {
1389
+ showToast('Generating API key...', 'info');
1390
+ const res = await fetch('/api/settings/api-key', {
1391
+ method: 'POST',
1392
+ headers: { 'Authorization': `Bearer ${authToken}` }
1393
+ });
1394
+ const data = await res.json();
1395
+ if (!data.success || !data.api_key) throw new Error(data.error || 'Failed to generate key');
1396
+ document.getElementById('apiKeyField').value = data.api_key;
1397
+ document.getElementById('apiKeyResult').classList.remove('hidden');
1398
+ document.getElementById('revokeApiKeyBtn').style.display = 'inline-block';
1399
+ showToast('API key generated — copy it now (shown once)', 'success');
1400
+ } catch (e) {
1401
+ showToast(`Key generation failed: ${e.message}`, 'error');
1402
+ }
1403
+ }
1404
+
1405
+ async function revokeApiKey() {
1406
+ try {
1407
+ const res = await fetch('/api/settings/api-key', {
1408
+ method: 'DELETE',
1409
+ headers: { 'Authorization': `Bearer ${authToken}` }
1410
+ });
1411
+ const data = await res.json();
1412
+ if (!data.success) throw new Error(data.error || 'Revoke failed');
1413
+ document.getElementById('apiKeyResult').classList.add('hidden');
1414
+ document.getElementById('revokeApiKeyBtn').style.display = 'none';
1415
+ showToast('API key revoked', 'success');
1416
+ } catch (e) {
1417
+ showToast(`Revoke failed: ${e.message}`, 'error');
1418
+ }
1419
+ }
1420
+
1421
+ function copyApiKey() {
1422
+ const f = document.getElementById('apiKeyField');
1423
+ if (f) { f.select(); navigator.clipboard.writeText(f.value); showToast('API key copied', 'success'); }
1424
+ }
1425
+
1426
+ function copyApiEndpoint() {
1427
+ const f = document.getElementById('apiEndpointField');
1428
+ if (f) { f.select(); navigator.clipboard.writeText(f.value); showToast('Endpoint copied', 'success'); }
1429
+ }
1430
+
1431
+ async function selfUpdate() {
1432
+ if (!confirm('Pull latest code from GitHub and restart the service?')) return;
1433
+ try {
1434
+ showToast('Updating & restarting…', 'info');
1435
+ const res = await fetch('/api/admin/update', {
1436
+ method: 'POST',
1437
+ headers: { 'Authorization': `Bearer ${authToken}` }
1438
+ });
1439
+ if (res.status === 403) { showToast('Founder access required', 'error'); return; }
1440
+ // On success the process re-execs; the connection drops. Reload after a beat.
1441
+ showToast('Update triggered — reloading…', 'success');
1442
+ setTimeout(() => location.reload(), 2500);
1443
+ } catch {
1444
+ // Expected: the old process is gone after re-exec, so the fetch may abort.
1445
+ showToast('Update triggered — reloading…', 'success');
1446
+ setTimeout(() => location.reload(), 2500);
1447
+ }
1448
+ }
1449
+
1450
  async function addMemoryFromTab() {
1451
  const input = document.getElementById('memoryInput');
1452
  const fact = input?.value.trim();
 
1607
  setTimeout(() => t.remove(), 4000);
1608
  }
1609
 
1610
+ async function openReleaseNotes() {
1611
+ const modal = document.getElementById('releaseNotesModal');
1612
+ const body = document.getElementById('releaseNotesBody');
1613
+ const title = document.getElementById('releaseNotesTitle');
1614
+ if (!modal || !body) return;
1615
+ body.innerHTML = '<div class="rn-meta">Loading…</div>';
1616
+ modal.classList.remove('hidden');
1617
+ try {
1618
+ const res = await fetch('/api/release-notes', { headers: { 'Authorization': `Bearer ${authToken}` } });
1619
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1620
+ const data = await res.json();
1621
+ title.textContent = data.title || '✨ What\'s New';
1622
+ let html = '';
1623
+ if (data.released) html += `<div class="rn-meta">Released ${escHtml(data.released)} · ${escHtml(data.version || '')}</div>`;
1624
+ (data.notes || []).forEach(n => {
1625
+ const tag = escHtml(n.tag || 'New');
1626
+ const t = escHtml(n.title || '');
1627
+ const b = escHtml(n.body || '');
1628
+ 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>`;
1629
+ });
1630
+ body.innerHTML = html || '<div class="rn-meta">No release notes available.</div>';
1631
+ } catch (e) {
1632
+ body.innerHTML = '<div class="rn-meta">Failed to load release notes.</div>';
1633
+ }
1634
+ }
1635
+
1636
  function escHtml(text) {
1637
  if (!text) return '';
1638
  const p = document.createElement('p');
 
1641
  }
1642
 
1643
  function fmt(text) {
1644
+ if (!text) return '';
1645
+ // First, extract and render markdown images ![alt](url) as real <img> tags.
1646
+ // We build the HTML in a safe way: escape everything, then swap image tokens.
1647
+ const imagePlaceholders = [];
1648
+ let escaped = escHtml(text);
1649
+ // Find markdown image syntax in the ORIGINAL text (before escaping) to get raw URLs
1650
+ const imgRegex = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1651
+ let m;
1652
+ while ((m = imgRegex.exec(text)) !== null) {
1653
+ const alt = m[1] || 'image';
1654
+ const url = m[2];
1655
+ // Only allow http(s) and same-origin relative paths (no javascript: etc.)
1656
+ if (/^(https?:\/\/|\/)/i.test(url)) {
1657
+ const token = `\u0000IMG${imagePlaceholders.length}\u0000`;
1658
+ imagePlaceholders.push(`<img class="gen-image" src="${escHtml(url)}" alt="${escHtml(alt)}" style="max-width:100%;border-radius:12px;margin:8px 0;">`);
1659
+ // Replace the markdown in the escaped string with the token
1660
+ const escapedMd = escHtml(m[0]);
1661
+ escaped = escaped.split(escapedMd).join(token);
1662
+ }
1663
+ }
1664
+ // Convert remaining markdown
1665
+ let out = escaped;
1666
  out = out.replace(/\n/g, '<br>');
1667
  out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1668
  out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
1669
+ // Restore image placeholders
1670
+ out = out.replace(/\u0000IMG(\d+)\u0000/g, (_, i) => imagePlaceholders[Number(i)] || '');
1671
  return out;
1672
  }
1673
 
 
1684
  // ========================================
1685
  document.addEventListener('DOMContentLoaded', () => {
1686
  console.log("NeuralAI Core UI Initializing...");
1687
+ // Restore saved theme (default to dark to match the app's design language)
1688
+ try {
1689
+ const saved = window.localStorage.getItem('neural_theme');
1690
+ if (saved === 'light') {
1691
+ document.body.classList.remove('dark-mode');
1692
+ } else {
1693
+ document.body.classList.add('dark-mode');
1694
+ }
1695
+ } catch (e) { document.body.classList.add('dark-mode'); }
1696
  initAuth();
1697
  initUploadListeners();
1698
 
 
1700
  document.getElementById('maestroSubmit')?.addEventListener('click', handleMaestroAuth);
1701
  document.getElementById('guestSubmit')?.addEventListener('click', handleGuestAuth);
1702
 
1703
+ // Wire top search bar (Google-style) — Ask button + Enter key
1704
+ document.getElementById('searchBtn')?.addEventListener('click', () => {
1705
+ console.log("Search Ask button clicked");
1706
+ handleSearch();
1707
+ });
1708
+ document.getElementById('queryInput')?.addEventListener('keydown', e => {
1709
+ if (e.key === 'Enter') {
1710
+ e.preventDefault();
1711
+ handleSearch();
1712
+ }
1713
+ });
1714
+
1715
+ // Wire bottom chat send button and enter key
1716
  document.getElementById('sendBtn')?.addEventListener('click', () => {
1717
  console.log("Send button clicked");
1718
  sendMessage();
 
1794
  sendMessage(card.dataset.prompt);
1795
  });
1796
  });
1797
+
1798
+ // Stop button (appears during streaming)
1799
+ document.getElementById('stopBtn')?.addEventListener('click', () => {
1800
+ abortStream = true;
1801
+ });
1802
+
1803
+ // Code editor modal wiring (triggered on request, not a composer button)
1804
+ document.getElementById('closeCodeModal')?.addEventListener('click', () => {
1805
+ document.getElementById('codeModal')?.classList.add('hidden');
1806
+ });
1807
+
1808
+ // Release Notes modal wiring
1809
+ document.getElementById('closeReleaseNotes')?.addEventListener('click', () => {
1810
+ document.getElementById('releaseNotesModal')?.classList.add('hidden');
1811
+ });
1812
+ document.getElementById('releaseNotesModal')?.addEventListener('click', (e) => {
1813
+ if (e.target.id === 'releaseNotesModal') e.target.classList.add('hidden');
1814
+ });
1815
+ document.getElementById('runCodeBtn')?.addEventListener('click', async () => {
1816
+ const editor = document.getElementById('codeEditor');
1817
+ const out = document.getElementById('codeOutput');
1818
+ if (!editor || !out) return;
1819
+ const code = editor.value;
1820
+ out.textContent = 'Running…';
1821
+ try {
1822
+ const res = await fetch('/api/execute/code', {
1823
+ method: 'POST',
1824
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
1825
+ body: JSON.stringify({ code, language: 'python' })
1826
+ });
1827
+ const data = await res.json();
1828
+ out.textContent = data.success ? data.output : (data.error || 'No output');
1829
+ // Also drop the result into the chat
1830
+ addMsg('assistant', '```python\n' + (data.success ? data.output : (data.error || 'No output')) + '\n```');
1831
+ } catch (e) {
1832
+ out.textContent = 'Error: ' + e.message;
1833
+ }
1834
+ });
1835
+
1836
+ // Uplink toggle
1837
+ const uplinkToggle = document.getElementById('uplinkToggle');
1838
+ if (uplinkToggle) {
1839
+ uplinkToggle.checked = uplinkEnabled;
1840
+ uplinkToggle.addEventListener('change', e => {
1841
+ uplinkEnabled = e.target.checked;
1842
+ window.localStorage.setItem('neural_uplink', uplinkEnabled ? '1' : '0');
1843
+ });
1844
+ }
1845
+ const savedUplink = window.localStorage.getItem('neural_uplink');
1846
+ if (savedUplink !== null) uplinkEnabled = savedUplink === '1';
1847
+
1848
+ // Model status polling
1849
+ updateModelStatus();
1850
+ setInterval(updateModelStatus, 30000);
1851
  });
1852
 
1853
  // Global Exports