nothumanallowed 9.7.2 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,10 +46,7 @@ input:focus,textarea:focus{border-color:var(--green3)}
46
46
 
47
47
  /* ---- LAYOUT: mobile-first ---- */
48
48
  .app{display:flex;flex-direction:column;height:100vh;height:100dvh}
49
- .header{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border);background:var(--bg);position:relative;z-index:60;flex-shrink:0}
50
- .header__burger{background:none;color:var(--green);font-size:22px;padding:4px 8px;line-height:1}
51
- .header__title{font-size:14px;color:var(--bright);font-weight:700;flex:1}
52
- .header__clock{font-size:10px;color:var(--dim)}
49
+ /* header removed info moved to sidebar brand */
53
50
 
54
51
  .sidebar{display:none;position:fixed;top:0;left:0;width:260px;height:100vh;height:100dvh;background:var(--bg2);border-right:1px solid var(--border);z-index:200;flex-direction:column;overflow-y:auto;box-shadow:4px 0 20px rgba(0,0,0,0.8)}
55
52
  .sidebar--open{display:flex}
@@ -87,8 +84,9 @@ input:focus,textarea:focus{border-color:var(--green3)}
87
84
  .section-title{font-size:12px;color:var(--cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
88
85
 
89
86
  /* ---- CHAT ---- */
90
- .chat{display:flex;flex-direction:column;height:calc(100vh - 52px);height:calc(100dvh - 52px)}
91
- @media(min-width:901px){.chat{height:calc(100vh - 52px)}}
87
+ .content--chat{overflow:hidden!important;padding:0!important;display:flex;flex-direction:column}
88
+ .chat{display:flex;flex-direction:column;flex:1;min-height:0;padding:16px;padding-bottom:0}
89
+ @media(min-width:901px){.content--chat{padding:0!important}}
92
90
  .chat__messages{flex:1;overflow-y:auto;padding-bottom:12px;-webkit-overflow-scrolling:touch}
93
91
  .chat__empty{text-align:center;padding:60px 16px;color:var(--dim)}
94
92
  .chat__empty-title{font-size:28px;color:var(--green);margin-bottom:12px}
@@ -96,14 +94,32 @@ input:focus,textarea:focus{border-color:var(--green3)}
96
94
  .msg{margin-bottom:12px}
97
95
  .msg--user .msg__bubble{background:var(--bg3);border:1px solid var(--border2);border-radius:8px 8px 2px 8px;padding:10px 14px;max-width:85%;margin-left:auto;color:var(--bright)}
98
96
  .msg--assistant .msg__bubble{background:var(--greendim);border:1px solid var(--green3);border-radius:8px 8px 8px 2px;padding:10px 14px;max-width:85%;color:var(--text);white-space:pre-wrap;word-wrap:break-word}
99
- .msg__actions{opacity:0.15;transition:opacity 0.2s}
100
- .msg:hover .msg__actions{opacity:1}
101
97
  .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
102
98
  .msg--thinking{color:var(--dim);font-style:italic}
103
- .chat__bar{display:flex;gap:8px;padding:10px 0 0 0;border-top:1px solid var(--border);flex-shrink:0}
99
+ .tool-indicator{display:inline-block;padding:2px 8px;margin:2px 0;border-radius:4px;font-size:11px;background:var(--bg3);border:1px solid var(--border)}
100
+ .tool-indicator--browser{border-color:#9c27b0;color:#ce93d8}
101
+ .tool-indicator--web{border-color:var(--cyan);color:var(--cyan)}
102
+ .tool-indicator--email{border-color:var(--green3);color:var(--green)}
103
+ .screenshot-preview{max-width:100%;border-radius:var(--r);margin:8px 0;border:1px solid var(--border)}
104
+ .browser-viewer{position:fixed;top:12px;left:12px;width:440px;background:var(--bg2);border:2px solid #9c27b0;border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.6);z-index:300;overflow:hidden;display:none;transition:all .3s ease}
105
+ .browser-viewer--open{display:block}
106
+ .browser-viewer__header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#1a1a2e;border-bottom:1px solid #9c27b0;font-size:10px;color:#ce93d8}
107
+ .browser-viewer__dot{width:6px;height:6px;border-radius:50%;background:#9c27b0;animation:bvpulse 1.5s infinite}
108
+ @keyframes bvpulse{0%,100%{opacity:1}50%{opacity:.3}}
109
+ .browser-viewer__title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
110
+ .browser-viewer__close{background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0 4px}
111
+ .browser-viewer__close:hover{color:#fff}
112
+ .browser-viewer__frame{width:100%;aspect-ratio:16/9;background:#000;display:flex;align-items:center;justify-content:center}
113
+ .browser-viewer__frame img{width:100%;height:100%;object-fit:contain}
114
+ .browser-viewer__status{padding:4px 10px;font-size:9px;color:var(--dim);border-top:1px solid var(--border)}
115
+ @media(max-width:600px){.browser-viewer{width:calc(100vw - 24px);top:8px;left:8px}}
116
+ @media(min-width:901px){.browser-viewer{left:232px}}
117
+ .chat__bar{display:flex;gap:8px;padding:10px 0 12px 0;border-top:1px solid var(--border);flex-shrink:0}
104
118
  .chat__input{flex:1;resize:none;min-height:40px;max-height:100px;padding:10px 14px}
105
119
  .chat__send{background:var(--green3);color:var(--bg);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
106
120
  .chat__send:disabled{opacity:.4}
121
+ .chat__stop{background:var(--red);color:var(--bright);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px;display:none}
122
+ .chat__stop--visible{display:block}
107
123
 
108
124
  /* ---- TASKS ---- */
109
125
  .task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
@@ -200,11 +216,52 @@ input:focus,textarea:focus{border-color:var(--green3)}
200
216
  const JS = `
201
217
  var API = '';
202
218
  var currentView = 'dashboard';
203
- var chatHistory = (function(){try{var s=localStorage.getItem('nha_chat_history');return s?JSON.parse(s):[];}catch(e){return [];}})();
219
+ var chatHistory = [];
220
+ var activeConvId = null;
221
+ var convList = [];
204
222
  var dash = {emails:[],events:[],tasks:[],plan:null,status:null};
223
+ var dashLoaded = {emails:false,events:false,tasks:false,contacts:false,notes:false,drive:false,github:false,notion:false,slack:false};
224
+ var chatStreaming = false;
225
+ var chatAbortController = null;
226
+
227
+ function endStreaming(){
228
+ chatStreaming=false;chatAbortController=null;
229
+ var stopBtn=document.getElementById('chatStop');if(stopBtn)stopBtn.classList.remove('chat__stop--visible');
230
+ var sendBtn=document.getElementById('chatSend');if(sendBtn)sendBtn.style.display='';
231
+ }
232
+ function stopChat(){
233
+ if(chatAbortController){try{chatAbortController.abort()}catch(e){}}
234
+ endStreaming();
235
+ if(chatHistory.length>0){
236
+ var last=chatHistory[chatHistory.length-1];
237
+ if(last.role==='assistant'&&(!last.content||last.content===''))last.content='[Stopped]';
238
+ }
239
+ renderMessages();
240
+ }
205
241
 
206
- function saveChatToStorage(){try{localStorage.setItem('nha_chat_history',JSON.stringify(chatHistory.slice(-40)));}catch(e){}}
207
- function clearChatHistory(){chatHistory=[];saveChatToStorage();renderMessages();}
242
+ // ---- BROWSER VIEWER (live preview of headless Chrome) ----
243
+ function showBrowserViewer(title,status){
244
+ var v=document.getElementById('browserViewer');if(!v)return;
245
+ v.classList.add('browser-viewer--open');
246
+ var t=document.getElementById('bvTitle');if(t)t.textContent=title||'Browser';
247
+ var s=document.getElementById('bvStatus');if(s)s.textContent=status||'Loading...';
248
+ }
249
+ function updateBrowserFrame(base64,format){
250
+ var f=document.getElementById('bvFrame');if(!f)return;
251
+ f.innerHTML='<img src="data:image/'+(format||'jpeg')+';base64,'+base64+'" alt="Browser view">';
252
+ }
253
+ function updateBrowserStatus(status){
254
+ var s=document.getElementById('bvStatus');if(s)s.textContent=status;
255
+ }
256
+ function closeBrowserViewer(){
257
+ var v=document.getElementById('browserViewer');if(v)v.classList.remove('browser-viewer--open');
258
+ }
259
+
260
+ function loadConvList(){return apiGet('/api/conversations').then(function(r){convList=(r&&r.conversations)||[];renderConvSidebar();})}
261
+ function loadConv(id){return apiGet('/api/conversations/'+id).then(function(r){if(r&&r.conversation){activeConvId=r.conversation.id;chatHistory=r.conversation.messages||[];renderMessages();renderConvSidebar();}})}
262
+ function createNewConv(){return apiPost('/api/conversations',{}).then(function(r){if(r&&r.conversation){activeConvId=r.conversation.id;chatHistory=[];renderMessages();loadConvList();}})}
263
+ function deleteConv(id){return fetch(API+'/api/conversations/'+id,{method:'DELETE'}).then(function(){loadConvList();if(id===activeConvId)createNewConv();})}
264
+ function clearChatHistory(){createNewConv()}
208
265
  var agentsList = [];
209
266
  var selectedAgent = null;
210
267
 
@@ -221,7 +278,11 @@ function switchView(v) {
221
278
  if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
222
279
  });
223
280
  var titles = {dashboard:'Dashboard',chat:'Chat',plan:'Daily Plan',tasks:'Tasks',emails:'Emails',calendar:'Calendar',drive:'Drive',contacts:'Contacts',notes:'Notes',onedrive:'OneDrive',mstodo:'Microsoft To Do',agents:'Agents',settings:'Settings'};
224
- document.getElementById('headerTitle').textContent = titles[v]||v;
281
+ var spt=document.getElementById('sidebarPageTitle');
282
+ if(spt)spt.textContent=titles[v]||v;
283
+ // Toggle content--chat class for proper chat layout (no overflow, flex column)
284
+ var ct=document.getElementById('content');
285
+ if(ct){if(v==='chat'){ct.classList.add('content--chat')}else{ct.classList.remove('content--chat')}}
225
286
  closeSidebar();
226
287
  render();
227
288
  }
@@ -253,10 +314,11 @@ function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){retur
253
314
 
254
315
  // ---- LOAD DATA ----
255
316
  function loadDash(){
256
- return Promise.all([apiGet('/api/status'),apiGet('/api/emails'),apiGet('/api/calendar'),apiGet('/api/tasks')]).then(function(r){
257
- dash.status=r[0];dash.emails=(r[1]&&r[1].emails)||[];dash.events=(r[2]&&r[2].events)||[];dash.tasks=(r[3]&&r[3].tasks)||[];
258
- updateBadges();
259
- });
317
+ // Load each API independently — render as each arrives (emails are slow)
318
+ apiGet('/api/status').then(function(r){dash.status=r;render()});
319
+ apiGet('/api/tasks').then(function(r){dash.tasks=(r&&r.tasks)||[];dashLoaded.tasks=true;updateBadges();render()});
320
+ apiGet('/api/calendar').then(function(r){dash.events=(r&&r.events)||[];dashLoaded.events=true;updateBadges();render()});
321
+ return apiGet('/api/emails?page=0&pageSize=25').then(function(r){dash.emails=(r&&r.emails)||[];dash._emailHasMore=r&&r.hasMore;dashLoaded.emails=true;emailPage=0;updateBadges();render()});
260
322
  }
261
323
  function loadAgents(){return apiGet('/api/agents').then(function(r){agentsList=(r&&r.agents)||[]})}
262
324
  function updateBadges(){
@@ -270,6 +332,7 @@ function updateBadges(){
270
332
  // ---- HELPERS ----
271
333
  function esc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
272
334
  function fmtTime(iso){if(!iso)return '';try{return new Date(iso).toLocaleTimeString('en',{hour:'2-digit',minute:'2-digit',hour12:true})}catch(e){return iso}}
335
+ function loadingHTML(label){return '<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim);margin-top:8px;font-size:12px">Loading '+esc(label)+'...</div></div>'}
273
336
 
274
337
  // ---- RENDER ----
275
338
  function render(){
@@ -298,13 +361,14 @@ function render(){
298
361
 
299
362
  // ---- DASHBOARD ----
300
363
  function renderDash(el){
364
+ if(!dashLoaded.tasks&&!dashLoaded.events&&!dashLoaded.emails){el.innerHTML=loadingHTML('dashboard');return}
301
365
  var t=dash.tasks,e=dash.emails,ev=dash.events;
302
366
  var done=t.filter(function(x){return x.status==='done'}).length;
303
367
  var pend=t.length-done;
304
368
  var pct=t.length>0?Math.round(done/t.length*100):0;
305
369
  var h='<div class="dash-grid">'+
306
370
  '<div class="card"><div class="card__title">Tasks</div><div class="card__value">'+pend+'</div><div class="card__sub">'+done+'/'+t.length+' done ('+pct+'%)</div></div>'+
307
- '<div class="card"><div class="card__title">Emails</div><div class="card__value">'+e.length+'</div><div class="card__sub">'+(e.length>0?esc(e[0].from):'Inbox zero')+'</div></div>'+
371
+ '<div class="card"><div class="card__title">Emails</div><div class="card__value">'+(dashLoaded.emails?e.length:'<span class="spinner" style="width:14px;height:14px;display:inline-block;vertical-align:middle"></span>')+'</div><div class="card__sub">'+(dashLoaded.emails?(e.length>0?esc(e[0].from):'Inbox zero'):'Loading...')+'</div></div>'+
308
372
  '<div class="card"><div class="card__title">Events</div><div class="card__value">'+ev.length+'</div><div class="card__sub">'+(ev.length>0?esc(ev[0].summary):'No events')+'</div></div>'+
309
373
  '<div class="card"><div class="card__title">Agents</div><div class="card__value">38</div><div class="card__sub">Ready</div></div>'+
310
374
  '</div>';
@@ -318,115 +382,225 @@ function renderDash(el){
318
382
  var chatReady=false;
319
383
  function renderChat(el){
320
384
  if(!chatReady||!document.getElementById('chatMessages')){
321
- el.innerHTML='<div class="chat"><div class="chat__messages" id="chatMessages"></div><div class="chat__bar"><button class="chat__mic" id="chatMic" onclick="toggleVoiceInput()" title="Voice input">&#127908;</button><textarea class="chat__input" id="chatInput" placeholder="Ask anything..." rows="1"></textarea><button class="chat__send" id="chatSend">Send</button><button onclick="clearChatHistory()" style="background:none;color:var(--dim);font-size:10px;padding:4px 8px" title="Clear chat history">Clear</button></div></div>';
385
+ el.innerHTML='<div style="display:flex;height:calc(100vh - 56px)">'+
386
+ '<div id="convSidebar" style="width:220px;border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0;background:var(--bg);display:'+(localStorage.getItem('nha_conv_sidebar')==='hidden'?'none':'')+'">'+
387
+ '<div style="padding:8px"><button onclick="createNewConv()" style="width:100%;padding:8px;border-radius:var(--r);border:1px solid var(--green);background:transparent;color:var(--green);cursor:pointer;font-size:11px">+ New Chat</button></div>'+
388
+ '<div id="convList"></div>'+
389
+ '</div>'+
390
+ '<div style="flex:1;display:flex;flex-direction:column;min-width:0">'+
391
+ '<div style="padding:6px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px">'+
392
+ '<button onclick="toggleConvSidebar()" style="background:none;border:none;cursor:pointer;font-size:14px;color:var(--dim);padding:2px 6px" title="Toggle conversations">&#128172;</button>'+
393
+ '<span id="convTitle" style="flex:1;font-size:12px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">New Chat</span>'+
394
+ '<button onclick="createNewConv()" style="background:none;border:1px solid var(--green);color:var(--green);padding:4px 10px;border-radius:var(--r);cursor:pointer;font-size:10px">+ New</button>'+
395
+ '<button onclick="exportConvMd()" style="background:none;border:1px solid var(--border);color:var(--dim);padding:4px 8px;border-radius:var(--r);cursor:pointer;font-size:10px" title="Export Markdown">Export</button>'+
396
+ '</div>'+
397
+ '<div class="chat"><div class="chat__messages" id="chatMessages"></div>'+
398
+ '<div id="chatAttachInfo" style="display:none;padding:4px 12px;font-size:11px;color:var(--cyan);background:var(--bg2);border-top:1px solid var(--border)"><span id="chatAttachName"></span> <button onclick="clearChatAttach()" style="background:none;border:none;color:#f44;cursor:pointer;font-size:14px;font-weight:700">&times;</button></div>'+
399
+ '<div class="chat__bar"><button class="chat__mic" id="chatMic" onclick="toggleVoiceInput()" title="Voice input">&#127908;</button><button onclick="document.getElementById(\\x27chatFileInput\\x27).click()" style="background:none;border:none;cursor:pointer;font-size:16px;padding:4px" title="Attach file">&#128206;</button><button onclick="document.getElementById(\\x27chatImageInput\\x27).click()" style="background:none;border:none;cursor:pointer;font-size:16px;padding:4px" title="Attach image">&#128247;</button><input type="file" id="chatFileInput" style="display:none" onchange="handleChatFile(this)"><input type="file" id="chatImageInput" accept="image/*" style="display:none" onchange="handleChatImage(this)"><textarea class="chat__input" id="chatInput" placeholder="Ask anything... (or attach file/image first)" rows="1"></textarea><button class="chat__send" id="chatSend">Send</button><button class="chat__stop" id="chatStop" onclick="stopChat()">Stop</button></div>'+
400
+ '</div>'+
401
+ '</div>'+
402
+ '</div>';
322
403
  chatReady=true;
323
404
  document.getElementById('chatSend').onclick=sendChat;
324
405
  document.getElementById('chatInput').onkeydown=function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}};
325
- renderMessages();
406
+ loadConvList().then(function(){
407
+ if(!activeConvId&&convList.length>0){loadConv(convList[0].id)}
408
+ else if(!activeConvId){createNewConv()}
409
+ else{loadConv(activeConvId)}
410
+ });
326
411
  setTimeout(function(){var i=document.getElementById('chatInput');if(i)i.focus()},100);
327
412
  }
328
413
  }
414
+ function toggleConvSidebar(){var s=document.getElementById('convSidebar');if(!s)return;var hide=s.style.display!=='none';s.style.display=hide?'none':'';try{localStorage.setItem('nha_conv_sidebar',hide?'hidden':'visible')}catch(e){}}
415
+ function renderConvSidebar(){
416
+ var el=document.getElementById('convList');if(!el)return;
417
+ var h='';convList.forEach(function(c){
418
+ var active=c.id===activeConvId;
419
+ var turns=Math.floor(c.messageCount/2);
420
+ h+='<div onclick="loadConv(\\x27'+c.id+'\\x27)" style="padding:8px 12px;cursor:pointer;border-left:3px solid '+(active?'var(--green)':'transparent')+';background:'+(active?'var(--bg2)':'transparent')+'" onmouseover="this.style.background=\\x27var(--bg2)\\x27" onmouseout="this.style.background='+(active?"\\x27var(--bg2)\\x27":"\\x27transparent\\x27")+'">'+
421
+ '<div style="font-size:11px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(c.title)+'</div>'+
422
+ '<div style="font-size:9px;color:var(--dim);display:flex;gap:6px;margin-top:2px"><span>'+turns+' turns</span>'+(active?'':'<span onclick="event.stopPropagation();deleteConv(\\x27'+c.id+'\\x27)" style="color:var(--red);cursor:pointer">del</span>')+'</div>'+
423
+ '</div>';
424
+ });
425
+ el.innerHTML=h;
426
+ var t=document.getElementById('convTitle');
427
+ if(t){var ac=convList.find(function(c){return c.id===activeConvId});t.textContent=ac?ac.title:'New Chat';}
428
+ }
429
+ function exportConvMd(){if(!activeConvId)return;window.open(API+'/api/conversations/'+activeConvId+'/export?format=md','_blank');}
329
430
  function renderMessages(){
330
431
  var el=document.getElementById('chatMessages');if(!el)return;
331
432
  if(chatHistory.length===0){
332
- el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations Assistant</div><div class="chat__empty-hint">Try: Show my unread emails / What is on my calendar? / Add a task</div></div>';
433
+ el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations Assistant — Streaming + Web Search + Browser</div><div class="chat__empty-hint">Try: Show my unread emails / Search the web for React 19 / Open google.com and take a screenshot</div></div>';
333
434
  return;
334
435
  }
335
- var h='';chatHistory.forEach(function(m,idx){
336
- var content = m.content || '';
337
- var isAssistant = m.role === 'assistant';
338
- var extraHtml = '';
339
-
340
- if (isAssistant) {
341
- // Handle canvas render markers
342
- var canvasMatch = content.match(/\\[CANVAS_RENDER\\]([\\s\\S]*?)\\[\\/CANVAS_RENDER\\]/);
343
- if (canvasMatch) {
344
- try { var cd = JSON.parse(canvasMatch[1]); showCanvas(cd.html, cd.title); } catch(e){}
345
- content = content.replace(/\\[CANVAS_RENDER\\][\\s\\S]*?\\[\\/CANVAS_RENDER\\]/, '').trim();
346
- }
347
- if (content.indexOf('[CANVAS_CLEAR]') !== -1) {
348
- closeCanvas();
349
- content = content.replace(/\\[CANVAS_CLEAR\\][\\s\\S]*?\\[\\/CANVAS_CLEAR\\]/, '').trim();
350
- }
351
- // Handle screenshot file markers
352
- var ssMatch = content.match(/\\[SCREENSHOT_FILE\\](.*?)\\[\\/SCREENSHOT_FILE\\]/);
353
- if (ssMatch) {
354
- var fname = ssMatch[1].split('/').pop();
355
- extraHtml = '<img src="/api/screenshots/' + encodeURIComponent(fname) + '" style="max-width:100%;border-radius:8px;margin:8px 0;border:1px solid rgba(0,255,65,0.2)" />';
356
- content = content.replace(/\\[SCREENSHOT_FILE\\].*?\\[\\/SCREENSHOT_FILE\\]/, '').trim();
357
- }
358
- }
359
-
360
- var bubbleContent = isAssistant ? extraHtml + esc(content).replace(/\\n/g, '<br>') : esc(content).replace(/\\n/g, '<br>');
361
-
362
- // Action buttons for each message
363
- var actions = '<div class="msg__actions" style="display:flex;gap:6px;margin-top:4px">';
364
- actions += '<button onclick="copyMessage('+idx+')" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;font-family:var(--mono)" title="Copy">Copy</button>';
365
- if (isAssistant) {
366
- actions += '<button onclick="retryMessage('+idx+')" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;font-family:var(--mono)" title="Retry">Retry</button>';
367
- } else {
368
- actions += '<button onclick="editMessage('+idx+')" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;font-family:var(--mono)" title="Edit">Edit</button>';
369
- }
370
- actions += '</div>';
371
-
372
- h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="msg__bubble">'+bubbleContent+'</div>'+actions+'</div>';
436
+ var h='';chatHistory.forEach(function(m){
437
+ var raw=m.content||'';
438
+ // Strip any raw base64 data that leaked into content (from LLM hallucinations)
439
+ raw=raw.replace(/data:image\\/[a-z]+;base64,[A-Za-z0-9+\\/=]{200,}/g,'[image]');
440
+ raw=raw.replace(/[A-Za-z0-9+\\/=]{500,}/g,'');
441
+ var imgs=[];var idx=0;
442
+ // Match both /api/screenshots/ URLs and data:image (short ones only, for inline display)
443
+ var safe=raw.replace(/!\\[([^\\]]*)\\]\\((\\/api\\/screenshots\\/[a-zA-Z0-9._-]+)\\)/g,function(_,alt,src){var ph='__IMG'+idx+'__';imgs.push({ph:ph,alt:alt,src:src});idx++;return ph;});
444
+ var content=esc(safe);
445
+ for(var i=0;i<imgs.length;i++){content=content.replace(imgs[i].ph,'<img class="screenshot-preview" alt="'+esc(imgs[i].alt)+'" src="'+imgs[i].src+'">');}
446
+ h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="msg__bubble">'+content+'</div></div>';
373
447
  });
374
448
  el.innerHTML=h;el.scrollTop=el.scrollHeight;
375
449
  }
376
- function copyMessage(idx){
377
- var m=chatHistory[idx];if(!m)return;
378
- var text=m.content.replace(/\\[SCREENSHOT_FILE\\].*?\\[\\/SCREENSHOT_FILE\\]/g,'').replace(/\\[CANVAS_RENDER\\][\\s\\S]*?\\[\\/CANVAS_RENDER\\]/g,'').trim();
379
- navigator.clipboard.writeText(text).then(function(){showToast('copy','Copied','Message copied to clipboard',2000)}).catch(function(){});
380
- }
381
- function retryMessage(idx){
382
- // Retry = re-send the user message that preceded this assistant message
383
- if(idx<1||chatHistory[idx].role!=='assistant')return;
384
- var userMsg=chatHistory[idx-1];
385
- if(!userMsg||userMsg.role!=='user')return;
386
- // Remove this assistant response and re-send
387
- chatHistory.splice(idx,1);
388
- saveChatToStorage();renderMessages();
389
- chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
390
- apiPost('/api/chat',{message:userMsg.content,history:chatHistory.slice(0,-1)}).then(function(r){
391
- chatHistory.pop();
392
- if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
393
- else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
394
- else{chatHistory.push({role:'assistant',content:'Error: no response from server'})}
395
- saveChatToStorage();renderMessages();
396
- });
450
+ var chatAttachedFile=null;
451
+ var chatAttachedImage=null;
452
+
453
+ function handleChatFile(input){
454
+ var file=input.files&&input.files[0];if(!file)return;
455
+ var isPDF=file.name.toLowerCase().endsWith('.pdf')||file.type==='application/pdf';
456
+ if(isPDF){
457
+ // PDF: read as base64 and send as document to LLM
458
+ var reader=new FileReader();
459
+ reader.onload=function(e){
460
+ var base64=e.target.result.split(',')[1];
461
+ chatAttachedFile={name:file.name,size:file.size,content:null,base64:base64,mimeType:'application/pdf',isPDF:true};
462
+ chatAttachedImage=null;
463
+ document.getElementById('chatAttachInfo').style.display='';
464
+ document.getElementById('chatAttachName').textContent='📎 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
465
+ };
466
+ reader.readAsDataURL(file);
467
+ }else{
468
+ var reader=new FileReader();
469
+ reader.onload=function(e){
470
+ chatAttachedFile={name:file.name,size:file.size,content:e.target.result};
471
+ chatAttachedImage=null;
472
+ document.getElementById('chatAttachInfo').style.display='';
473
+ document.getElementById('chatAttachName').textContent='📎 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
474
+ };
475
+ reader.readAsText(file);
476
+ }
397
477
  }
398
- function editMessage(idx){
399
- if(chatHistory[idx].role!=='user')return;
400
- var inp=document.getElementById('chatInput');if(!inp)return;
401
- inp.value=chatHistory[idx].content;
402
- inp.focus();
403
- // Remove this message and all subsequent messages
404
- chatHistory.splice(idx);
405
- saveChatToStorage();renderMessages();
478
+
479
+ function handleChatImage(input){
480
+ var file=input.files&&input.files[0];if(!file)return;
481
+ var reader=new FileReader();
482
+ reader.onload=function(e){
483
+ var base64=e.target.result.split(',')[1];
484
+ chatAttachedImage={name:file.name,size:file.size,base64:base64,mimeType:file.type||'image/jpeg'};
485
+ chatAttachedFile=null;
486
+ document.getElementById('chatAttachInfo').style.display='';
487
+ document.getElementById('chatAttachName').textContent='📷 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
488
+ };
489
+ reader.readAsDataURL(file);
490
+ }
491
+
492
+ function clearChatAttach(){
493
+ chatAttachedFile=null;chatAttachedImage=null;
494
+ document.getElementById('chatAttachInfo').style.display='none';
495
+ document.getElementById('chatFileInput').value='';
496
+ document.getElementById('chatImageInput').value='';
406
497
  }
498
+
407
499
  function sendChat(){
408
500
  var inp=document.getElementById('chatInput');if(!inp)return;
409
- var msg=inp.value.trim();if(!msg)return;
410
- chatHistory.push({role:'user',content:msg});
411
- inp.value='';saveChatToStorage();renderMessages();
412
- chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
413
- apiPost('/api/chat',{message:msg,history:chatHistory.slice(0,-1)}).then(function(r){
414
- chatHistory.pop();
415
- if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
416
- else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
417
- else{chatHistory.push({role:'assistant',content:'Error: no response from server'})}
418
- saveChatToStorage();renderMessages();
419
- // Refresh ALL data after any tool execution
420
- if(r&&((r.actions&&r.actions.length>0)||(r.toolResults&&r.toolResults.length>0))){
421
- calEventsCache={};
422
- contactsData=null;
423
- notesData=null;
424
- driveData=null;
425
- onedriveData=null;
426
- mstodoData=null;
427
- loadDash().then(function(){render()}).catch(function(){});
501
+ var msg=inp.value.trim();
502
+ var hasAttach=!!chatAttachedFile||!!chatAttachedImage;
503
+ if(!msg&&!hasAttach)return;
504
+ if(chatStreaming)return;
505
+
506
+ var displayMsg=msg;
507
+ if(chatAttachedFile)displayMsg=(msg?msg+' ':'')+'[File: '+chatAttachedFile.name+']';
508
+ if(chatAttachedImage)displayMsg=(msg?msg+' ':'')+'[Image: '+chatAttachedImage.name+']';
509
+
510
+ chatHistory.push({role:'user',content:displayMsg});
511
+ inp.value='';renderMessages();
512
+
513
+ // If attachment, use regular (non-streaming) endpoint
514
+ if(chatAttachedFile||chatAttachedImage){
515
+ chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
516
+ var payload={message:msg||'Analyze this attachment',history:chatHistory.slice(0,-1)};
517
+ if(chatAttachedFile){
518
+ if(chatAttachedFile.isPDF&&chatAttachedFile.base64){payload.pdfBase64=chatAttachedFile.base64;payload.pdfName=chatAttachedFile.name;}
519
+ else{payload.fileContent=chatAttachedFile.content;payload.fileName=chatAttachedFile.name;}
428
520
  }
429
- });
521
+ if(chatAttachedImage){payload.imageBase64=chatAttachedImage.base64;payload.imageMimeType=chatAttachedImage.mimeType;}
522
+ clearChatAttach();
523
+ apiPost('/api/chat',payload).then(function(r){
524
+ chatHistory.pop();
525
+ if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
526
+ else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
527
+ else{chatHistory.push({role:'assistant',content:'Error: no response'})}
528
+ renderMessages();loadConvList();
529
+ });
530
+ return;
531
+ }
532
+ clearChatAttach();
533
+
534
+ // Streaming SSE
535
+ chatStreaming=true;
536
+ chatAbortController=new AbortController();
537
+ // Show Stop button, hide Send button
538
+ var stopBtn=document.getElementById('chatStop');if(stopBtn)stopBtn.classList.add('chat__stop--visible');
539
+ var sendBtn=document.getElementById('chatSend');if(sendBtn)sendBtn.style.display='none';
540
+ chatHistory.push({role:'assistant',content:''});
541
+ renderMessages();
542
+ var streamIdx=chatHistory.length-1;
543
+ var allHistory=chatHistory.slice(0,-1).map(function(m){return{role:m.role,content:(m.content||'').replace(/!\\[Screenshot\\]\\(data:image\\/[^)]+\\)/g,'[Screenshot taken]')};});
544
+ var payload={message:msg,history:allHistory,conversationId:activeConvId};
545
+
546
+ fetch(API+'/api/chat/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:chatAbortController.signal}).then(function(response){
547
+ if(!response.ok||!response.body){chatHistory[streamIdx].content='Error: connection failed';endStreaming();renderMessages();return;}
548
+ var reader=response.body.getReader();var decoder=new TextDecoder();var buffer='';var currentEvent='';
549
+ function pump(){
550
+ reader.read().then(function(result){
551
+ if(result.done){endStreaming();renderMessages();loadConvList();return;}
552
+ buffer+=decoder.decode(result.value,{stream:true});
553
+ var lines=buffer.split('\\n');buffer=lines.pop()||'';
554
+ for(var i=0;i<lines.length;i++){
555
+ var line=lines[i];
556
+ if(line.startsWith('event: ')){currentEvent=line.slice(7).trim();}
557
+ else if(line.startsWith('data: ')){
558
+ try{
559
+ var data=JSON.parse(line.slice(6));
560
+ if(currentEvent==='token'&&data.content){
561
+ chatHistory[streamIdx].content+=data.content;
562
+ var el=document.getElementById('chatMessages');
563
+ if(el){var msgs=el.querySelectorAll('.msg');var last=msgs[msgs.length-1];if(last){var bub=last.querySelector('.msg__bubble');if(bub)bub.textContent=chatHistory[streamIdx].content;}el.scrollTop=el.scrollHeight;}
564
+ }
565
+ if(currentEvent==='tool'){
566
+ var toolLabels={browser_open:'Opening page',browser_screenshot:'Taking screenshot',browser_click:'Clicking element',browser_type:'Typing text',browser_extract:'Extracting content',browser_js:'Running JavaScript',browser_wait:'Waiting for element',browser_scroll:'Scrolling page',browser_key:'Pressing key',browser_close:'Closing browser',web_search:'Searching the web',fetch_url:'Fetching URL',gmail_list:'Searching emails',gmail_read:'Reading email',gmail_send:'Sending email',calendar_today:'Loading calendar',calendar_create:'Creating event'};
567
+ var label=toolLabels[data.action]||data.action;
568
+ var indicator=data.status==='executing'?'\\u23f3 '+label+'...':'\\u2705 '+label;
569
+ if(data.status==='error')indicator='\\u274c '+label+' failed';
570
+ // Show browser viewer for browser and web_search actions
571
+ var isBrowserAction=data.action&&(data.action.startsWith('browser_')||data.action==='web_search');
572
+ if(isBrowserAction&&data.status==='executing'){showBrowserViewer(label,'Executing...');}
573
+ if(isBrowserAction&&data.status==='done'){updateBrowserStatus('\\u2705 '+label);}
574
+ if(isBrowserAction&&data.status==='error'){updateBrowserStatus('\\u274c '+label);}
575
+ if(data.action==='browser_close'&&data.status==='done'){setTimeout(closeBrowserViewer,2000);}
576
+ // Strip JSON action blocks from streamed content (they are internal tool calls, not for the user)
577
+ if(data.status==='executing'){chatHistory[streamIdx].content=chatHistory[streamIdx].content.replace(new RegExp('\\x60\\x60\\x60json[\\\\s\\\\S]*?\\x60\\x60\\x60','g'),'').trim()+'\\n';}
578
+ chatHistory[streamIdx].content+=indicator+'\\n';
579
+ renderMessages();
580
+ }
581
+ if(currentEvent==='screenshot'&&data.base64){
582
+ // Only update the browser viewer — the actual image in chat is handled by the 'done' event via screenshotFiles
583
+ showBrowserViewer('Screenshot','Captured');
584
+ updateBrowserFrame(data.base64,data.format||'jpeg');
585
+ updateBrowserStatus('Screenshot captured');
586
+ }
587
+ if(currentEvent==='browser_frame'&&data.base64){
588
+ // Live frame update — also ensure viewer is open
589
+ showBrowserViewer(data.url||'Browser','Live');
590
+ updateBrowserFrame(data.base64,data.format||'jpeg');
591
+ if(data.url)updateBrowserStatus(data.url);
592
+ }
593
+ if(currentEvent==='tool_synthesis'){chatHistory[streamIdx].content='';renderMessages();}
594
+ if(currentEvent==='done'){endStreaming();if(data.content)chatHistory[streamIdx].content=data.content;var ssf=data.screenshotFiles||[];for(var fi=0;fi<ssf.length;fi++){chatHistory[streamIdx].content+='\\n![Screenshot](/api/screenshots/'+ssf[fi]+')\\n';}renderMessages();loadConvList();if(ssf.length>0)setTimeout(closeBrowserViewer,5000);}
595
+ if(currentEvent==='error'){endStreaming();chatHistory[streamIdx].content='Error: '+(data.message||'Unknown');renderMessages();}
596
+ }catch(e){}
597
+ }
598
+ }
599
+ pump();
600
+ }).catch(function(e){endStreaming();if(e.name!=='AbortError'){chatHistory[streamIdx].content='Error: '+e.message;renderMessages();}});
601
+ }
602
+ pump();
603
+ }).catch(function(e){endStreaming();if(e.name!=='AbortError'){chatHistory[streamIdx].content='Error: '+e.message;renderMessages();}});
430
604
  }
431
605
 
432
606
  // ---- TASKS ----
@@ -496,17 +670,64 @@ function refreshPlan(){
496
670
 
497
671
  // ---- EMAILS ----
498
672
  function renderEmails(el){
673
+ if(!dashLoaded.emails){el.innerHTML=loadingHTML('emails');return}
499
674
  var e=dash.emails;
500
- if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No unread emails</div>';return}
501
- var h='';e.forEach(function(x){
675
+ if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">Inbox zero — no emails</div>';return}
676
+ var unreadCount=e.filter(function(x){return x.isUnread}).length;
677
+ var h='<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">';
678
+ h+='<span style="font-size:12px;color:var(--dim)">'+e.length+' emails'+(unreadCount>0?' ('+unreadCount+' unread)':'')+'</span>';
679
+ if(unreadCount>0)h+='<button class="btn btn--secondary" style="font-size:10px;padding:4px 10px" onclick="markAllEmailsRead()">Mark all read</button>';
680
+ h+='</div>';
681
+ e.forEach(function(x){
502
682
  var unreadStyle=x.isUnread?'border-left:3px solid var(--green);font-weight:700':'border-left:3px solid transparent;opacity:0.7';
503
683
  h+='<div class="card email" style="cursor:pointer;'+unreadStyle+'" onclick="openEmail(\\x27'+esc(x.id)+'\\x27)"><div class="email__header"><span class="email__from">'+esc(x.from)+'</span><span class="email__date">'+esc(x.date)+(x.isUnread?' <span style="color:var(--green);font-size:9px">NEW</span>':'')+'</span></div><div class="email__subject">'+esc(x.subject)+'</div><div class="email__snippet" style="font-weight:400">'+esc((x.snippet||'').slice(0,150))+'</div></div>';
504
684
  });
685
+ // Load More button
686
+ if(dash._emailHasMore!==false){
687
+ h+='<button id="loadMoreEmails" onclick="loadMoreEmails()" style="width:100%;padding:12px;margin-top:8px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);color:var(--cyan);font-family:var(--font);font-size:12px;cursor:pointer;font-weight:700">Load More Emails</button>';
688
+ }
505
689
  el.innerHTML=h;
506
690
  }
691
+ var emailPage=0;
692
+ function loadMoreEmails(){
693
+ var btn=document.getElementById('loadMoreEmails');
694
+ if(btn){btn.textContent='Loading...';btn.disabled=true;}
695
+ emailPage++;
696
+ apiGet('/api/emails?page='+emailPage+'&pageSize=25').then(function(r){
697
+ if(r&&r.emails){
698
+ for(var i=0;i<r.emails.length;i++){
699
+ if(!dash.emails.find(function(e){return e.id===r.emails[i].id})){
700
+ dash.emails.push(r.emails[i]);
701
+ }
702
+ }
703
+ dash._emailHasMore=r.hasMore;
704
+ updateBadges();
705
+ render();
706
+ }
707
+ });
708
+ }
709
+ function markAllEmailsRead(){
710
+ apiPost('/api/email/mark-all-read',{}).then(function(r){
711
+ if(r&&r.ok){
712
+ dash.emails.forEach(function(e){e.isUnread=false});
713
+ updateBadges();
714
+ renderEmails(document.getElementById('content'));
715
+ showToast('success','All Read','Marked '+( r.count||0)+' emails as read');
716
+ }else{
717
+ showToast('error','Error',r&&r.error||'Failed');
718
+ }
719
+ });
720
+ }
507
721
  var openEmailId=null;
508
722
  function openEmail(id){
509
723
  openEmailId=id;
724
+ // Mark as read locally + on server
725
+ var emailObj=dash.emails.find(function(e){return e.id===id});
726
+ if(emailObj&&emailObj.isUnread){
727
+ emailObj.isUnread=false;
728
+ updateBadges();
729
+ apiPost('/api/email/mark-read',{messageId:id}).catch(function(){});
730
+ }
510
731
  var el=document.getElementById('content');
511
732
  el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading email...</div></div>';
512
733
  apiPost('/api/email/read',{messageId:id}).then(function(r){
@@ -1155,22 +1376,68 @@ function renderAgents(el){
1155
1376
 
1156
1377
  var filtered=agentFilter?agentsList.filter(function(a){return a.category===agentFilter}):agentsList;
1157
1378
 
1379
+ h+='<div style="margin-bottom:10px"><button class="btn btn--primary" style="font-size:11px" onclick="showCreateAgentForm()">+ Create Agent</button></div>';
1158
1380
  h+='<div class="agents-grid">';
1159
1381
  filtered.forEach(function(a){
1160
1382
  var name=a.name||a.agentName;
1161
1383
  var display=a.displayName||name;
1162
1384
  var icon=AGENT_ICONS[name.toLowerCase()]||'\\u{1F916}';
1163
1385
  var desc=AGENT_DESCRIPTIONS[name.toLowerCase()]||a.tagline||a.description||'';
1164
- h+='<div class="card agent-card" onclick="openAgent(\\''+esc(name)+'\\',\\''+esc(display)+'\\')">'+
1386
+ var isCustom=a.category==='custom';
1387
+ h+='<div class="card agent-card" style="position:relative">'+
1388
+ '<div style="flex:1;cursor:pointer" onclick="openAgent(\\''+esc(name)+'\\',\\''+esc(display)+'\\')">'+
1389
+ '<div style="display:flex;align-items:center;gap:8px">'+
1165
1390
  '<div class="agent-card__icon">'+icon+'</div>'+
1166
1391
  '<div class="agent-card__body"><div class="agent-card__name">'+esc(display)+'</div>'+
1167
1392
  '<div class="agent-card__tagline">'+esc(desc)+'</div></div>'+
1393
+ '</div></div>'+
1394
+ '<div style="display:flex;gap:4px;flex-shrink:0">'+
1395
+ '<button onclick="editAgent(\\''+esc(name)+'\\')" style="background:none;border:none;cursor:pointer;font-size:12px;padding:2px" title="Edit">\\u{270F}\\u{FE0F}</button>'+
1396
+ '<button onclick="deleteAgent(\\''+esc(name)+'\\')" style="background:none;border:none;cursor:pointer;font-size:12px;padding:2px;color:#f44" title="Delete">\\u{1F5D1}</button>'+
1397
+ '</div>'+
1168
1398
  '</div>';
1169
1399
  });
1170
1400
  h+='</div>';
1171
1401
  el.innerHTML=h;
1172
1402
  }
1173
1403
  var agentFilter=null;
1404
+
1405
+ function showCreateAgentForm(){
1406
+ var name=prompt('Agent name (lowercase, no spaces):');
1407
+ if(!name)return;
1408
+ name=name.toLowerCase().replace(/[^a-z0-9_-]/g,'');
1409
+ if(!name)return;
1410
+ var tagline=prompt('Tagline (short description):');
1411
+ if(!tagline)return;
1412
+ var sysPrompt=prompt('System prompt (agent personality & instructions):');
1413
+ if(!sysPrompt)return;
1414
+ apiPost('/api/agents',{name:name,tagline:tagline,systemPrompt:sysPrompt}).then(function(r){
1415
+ if(r&&r.ok){showToast('success','Agent Created',name.toUpperCase()+' is ready to use');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
1416
+ else{alert('Error: '+(r&&r.error||'Unknown'));}
1417
+ });
1418
+ }
1419
+
1420
+ function editAgent(name){
1421
+ fetch('/api/agents/'+name).then(function(r){return r.json()}).then(function(data){
1422
+ var newTagline=prompt('Tagline:',data.tagline||'');
1423
+ if(newTagline===null)return;
1424
+ var newPrompt=prompt('System prompt:',data.systemPrompt||'');
1425
+ if(newPrompt===null)return;
1426
+ fetch('/api/agents/'+name,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({tagline:newTagline,systemPrompt:newPrompt,category:data.category||'custom'})}).then(function(r){return r.json()}).then(function(r){
1427
+ if(r&&r.ok){showToast('success','Agent Updated',name.toUpperCase()+' updated');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
1428
+ else{alert('Error: '+(r&&r.error||'Unknown'));}
1429
+ });
1430
+ });
1431
+ }
1432
+
1433
+ function deleteAgent(name){
1434
+ if(!confirm('Delete agent "'+name+'"? This cannot be undone.'))return;
1435
+ fetch('/api/agents/'+name,{method:'DELETE'}).then(function(r){return r.json()}).then(function(r){
1436
+ if(r&&r.ok){showToast('success','Agent Deleted',name+' removed');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
1437
+ else{alert('Error: '+(r&&r.error||'Unknown'));}
1438
+ });
1439
+ }
1440
+
1174
1441
  function openAgent(name,display){
1175
1442
  selectedAgent=name;
1176
1443
  attachedFileContent=null;attachedFileName=null;
@@ -1253,9 +1520,26 @@ function renderSettings(el) {
1253
1520
  ['summary-time', 'Summary Time', '18:00'],
1254
1521
  ['meeting-alert', 'Meeting Alert (minutes)', '30'],
1255
1522
  ]) +
1523
+ '<div class="card" style="margin-top:16px"><div class="card__title">Google Account</div>' +
1524
+ '<div style="font-size:11px;color:var(--dim);margin-bottom:8px">Connect Gmail, Calendar, Drive, Contacts, and Tasks. Opens a browser window for Google sign-in.</div>' +
1525
+ (settingsData.hasGoogle ? '<div style="color:var(--green);font-size:12px;margin-bottom:8px">\\u2705 Connected</div>' : '') +
1526
+ '<button onclick="connectGoogle()" style="background:var(--green3);color:var(--bg);padding:8px 20px;border-radius:var(--r);font-weight:700;font-size:12px;cursor:pointer;border:none">' + (settingsData.hasGoogle ? 'Reconnect Google' : 'Connect Google') + '</button>' +
1527
+ '<div id="googleStatus" style="margin-top:8px;font-size:10px;color:var(--dim)"></div>' +
1528
+ '</div>' +
1256
1529
  '</div>';
1257
1530
  }
1258
1531
 
1532
+ function connectGoogle() {
1533
+ var s = document.getElementById('googleStatus');
1534
+ if (s) s.textContent = 'Starting Google sign-in...';
1535
+ apiPost('/api/google/auth', {}).then(function(r) {
1536
+ if (s) s.textContent = r.message || 'Check the browser window that opened.';
1537
+ if (s) s.style.color = 'var(--green)';
1538
+ }).catch(function(e) {
1539
+ if (s) { s.textContent = 'Error: ' + e.message; s.style.color = 'var(--red)'; }
1540
+ });
1541
+ }
1542
+
1259
1543
  function settingsSection(id, title, desc, fields) {
1260
1544
  var h = '<form class="card" style="margin-bottom:16px" id="settings-' + id + '" onsubmit="event.preventDefault();saveSettingsSection(\\x27' + id + '\\x27)">' +
1261
1545
  '<div class="card__title" style="color:var(--green);font-size:14px;margin-bottom:4px">' + esc(title) + '</div>' +
@@ -1476,61 +1760,6 @@ function handleDaemonEvent(msg) {
1476
1760
  showToast('plan', 'Daily Plan Ready', 'Your plan for ' + msg.data.date + ' has been generated.', 10000);
1477
1761
  if (currentView === 'plan') renderPlan(document.getElementById('content'));
1478
1762
  break;
1479
-
1480
- case 'cron_result':
1481
- showToast('cron', 'Scheduled Task', msg.data.prompt + '\\n' + (msg.data.result || '').slice(0, 100), 8000);
1482
- break;
1483
- }
1484
- }
1485
-
1486
- // ---- CANVAS PANEL ----
1487
- var canvasVisible = false;
1488
- function showCanvas(html, title) {
1489
- var panel = document.getElementById('canvasPanel');
1490
- if (!panel) {
1491
- panel = document.createElement('div');
1492
- panel.id = 'canvasPanel';
1493
- panel.style.cssText = 'position:fixed;top:60px;right:12px;width:480px;max-height:calc(100vh - 80px);background:#0d0d0d;border:1px solid var(--green);border-radius:12px;z-index:1000;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 0 30px rgba(0,255,65,0.1)';
1494
- document.body.appendChild(panel);
1495
- }
1496
- var header = '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--green);background:rgba(0,255,65,0.05)">' +
1497
- '<span style="font-family:var(--mono);color:var(--green);font-size:12px">' + (title || 'Canvas') + '</span>' +
1498
- '<div><button onclick="toggleCanvasSize()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;margin-right:8px" title="Resize">&#x2922;</button>' +
1499
- '<button onclick="closeCanvas()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Close">&times;</button></div></div>';
1500
- var iframe = '<iframe id="canvasFrame" sandbox="allow-scripts allow-same-origin" style="flex:1;border:none;background:#fff;min-height:300px;width:100%"></iframe>';
1501
- panel.innerHTML = header + iframe;
1502
- panel.style.display = 'flex';
1503
- canvasVisible = true;
1504
-
1505
- // Write HTML to iframe
1506
- var frame = document.getElementById('canvasFrame');
1507
- if (frame) {
1508
- var doc = frame.contentDocument || frame.contentWindow.document;
1509
- doc.open();
1510
- doc.write(html);
1511
- doc.close();
1512
- }
1513
- }
1514
-
1515
- function closeCanvas() {
1516
- var panel = document.getElementById('canvasPanel');
1517
- if (panel) { panel.style.display = 'none'; canvasVisible = false; }
1518
- }
1519
-
1520
- function toggleCanvasSize() {
1521
- var panel = document.getElementById('canvasPanel');
1522
- if (!panel) return;
1523
- if (panel.style.width === '480px') {
1524
- panel.style.width = '80vw';
1525
- panel.style.height = '80vh';
1526
- panel.style.top = '10vh';
1527
- panel.style.right = '10vw';
1528
- } else {
1529
- panel.style.width = '480px';
1530
- panel.style.height = '';
1531
- panel.style.maxHeight = 'calc(100vh - 80px)';
1532
- panel.style.top = '60px';
1533
- panel.style.right = '12px';
1534
1763
  }
1535
1764
  }
1536
1765
 
@@ -1636,8 +1865,13 @@ init();
1636
1865
  <div class="app">
1637
1866
  <nav class="sidebar" id="sidebar">
1638
1867
  <div class="sidebar__brand">
1639
- <div class="sidebar__brand-name">NHA</div>
1640
- <div class="sidebar__brand-sub">Operations Console</div>
1868
+ <div style="display:flex;align-items:center;gap:8px">
1869
+ <div class="sidebar__brand-name">NHA</div>
1870
+ <span id="wsIndicator" style="color:var(--dim);font-size:8px" title="Daemon WebSocket">&#9679;</span>
1871
+ <span style="font-size:9px;color:var(--dim)">v${VERSION}</span>
1872
+ </div>
1873
+ <div id="sidebarPageTitle" style="font-size:11px;color:var(--bright);margin-top:4px;font-weight:600">Dashboard</div>
1874
+ <div class="sidebar__brand-sub" id="clock"></div>
1641
1875
  </div>
1642
1876
  <div class="sidebar__section">
1643
1877
  <div class="sidebar__label">Overview</div>
@@ -1674,17 +1908,30 @@ init();
1674
1908
  <div class="sidebar__label">Config</div>
1675
1909
  <div class="nav-item" data-view="settings" onclick="switchView('settings')"><span class="nav-item__icon">&#9881;</span> Settings</div>
1676
1910
  </div>
1677
- <div style="padding:12px 16px;margin-top:auto;border-top:1px solid var(--border);font-size:10px;color:var(--dim)">NHA v${VERSION}</div>
1911
+ <div class="sidebar__section">
1912
+ <div class="sidebar__label">Help</div>
1913
+ <a href="https://nothumanallowed.com/docs" target="_blank" class="nav-item" style="text-decoration:none"><span class="nav-item__icon">&#128214;</span> Documentation</a>
1914
+ <a href="https://nothumanallowed.com/docs/agents" target="_blank" class="nav-item" style="text-decoration:none"><span class="nav-item__icon">&#129302;</span> Agents Guide</a>
1915
+ <a href="https://nothumanallowed.com/docs/mobile" target="_blank" class="nav-item" style="text-decoration:none"><span class="nav-item__icon">&#128241;</span> Mobile App</a>
1916
+ </div>
1917
+ <div style="padding:12px 16px;margin-top:auto;border-top:1px solid var(--border);font-size:10px;color:var(--dim)">nothumanallowed.com</div>
1678
1918
  </nav>
1679
1919
 
1680
- <div class="header">
1681
- <button class="header__burger" onclick="toggleSidebar()">&#9776;</button>
1682
- <span class="header__title" id="headerTitle">Dashboard</span>
1683
- <span id="wsIndicator" style="color:var(--dim);font-size:8px;margin-right:4px" title="Daemon WebSocket">&#9679;</span>
1684
- <span class="header__clock" id="clock"></span>
1685
- </div>
1920
+ <button onclick="openSidebar()" style="position:fixed;top:8px;left:8px;z-index:100;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);color:var(--green);font-size:20px;padding:4px 10px;cursor:pointer;line-height:1" id="mobileBurger">&#9776;</button>
1686
1921
 
1687
1922
  <div class="content" id="content"></div>
1923
+
1924
+ <div class="browser-viewer" id="browserViewer">
1925
+ <div class="browser-viewer__header">
1926
+ <span class="browser-viewer__dot"></span>
1927
+ <span class="browser-viewer__title" id="bvTitle">Browser</span>
1928
+ <button class="browser-viewer__close" onclick="closeBrowserViewer()">&times;</button>
1929
+ </div>
1930
+ <div class="browser-viewer__frame" id="bvFrame">
1931
+ <span style="color:var(--dim);font-size:11px">Waiting...</span>
1932
+ </div>
1933
+ <div class="browser-viewer__status" id="bvStatus">Idle</div>
1934
+ </div>
1688
1935
  </div>
1689
1936
 
1690
1937
  <div class="modal-overlay" id="agentModal">