nothumanallowed 9.7.1 → 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}
@@ -98,10 +96,30 @@ input:focus,textarea:focus{border-color:var(--green3)}
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
97
  .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
100
98
  .msg--thinking{color:var(--dim);font-style:italic}
101
- .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}
102
118
  .chat__input{flex:1;resize:none;min-height:40px;max-height:100px;padding:10px 14px}
103
119
  .chat__send{background:var(--green3);color:var(--bg);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
104
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}
105
123
 
106
124
  /* ---- TASKS ---- */
107
125
  .task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
@@ -198,11 +216,52 @@ input:focus,textarea:focus{border-color:var(--green3)}
198
216
  const JS = `
199
217
  var API = '';
200
218
  var currentView = 'dashboard';
201
- 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 = [];
202
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
+ }
203
241
 
204
- function saveChatToStorage(){try{localStorage.setItem('nha_chat_history',JSON.stringify(chatHistory.slice(-40)));}catch(e){}}
205
- 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()}
206
265
  var agentsList = [];
207
266
  var selectedAgent = null;
208
267
 
@@ -219,7 +278,11 @@ function switchView(v) {
219
278
  if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
220
279
  });
221
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'};
222
- 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')}}
223
286
  closeSidebar();
224
287
  render();
225
288
  }
@@ -251,10 +314,11 @@ function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){retur
251
314
 
252
315
  // ---- LOAD DATA ----
253
316
  function loadDash(){
254
- return Promise.all([apiGet('/api/status'),apiGet('/api/emails'),apiGet('/api/calendar'),apiGet('/api/tasks')]).then(function(r){
255
- 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)||[];
256
- updateBadges();
257
- });
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()});
258
322
  }
259
323
  function loadAgents(){return apiGet('/api/agents').then(function(r){agentsList=(r&&r.agents)||[]})}
260
324
  function updateBadges(){
@@ -268,6 +332,7 @@ function updateBadges(){
268
332
  // ---- HELPERS ----
269
333
  function esc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
270
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>'}
271
336
 
272
337
  // ---- RENDER ----
273
338
  function render(){
@@ -296,13 +361,14 @@ function render(){
296
361
 
297
362
  // ---- DASHBOARD ----
298
363
  function renderDash(el){
364
+ if(!dashLoaded.tasks&&!dashLoaded.events&&!dashLoaded.emails){el.innerHTML=loadingHTML('dashboard');return}
299
365
  var t=dash.tasks,e=dash.emails,ev=dash.events;
300
366
  var done=t.filter(function(x){return x.status==='done'}).length;
301
367
  var pend=t.length-done;
302
368
  var pct=t.length>0?Math.round(done/t.length*100):0;
303
369
  var h='<div class="dash-grid">'+
304
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>'+
305
- '<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>'+
306
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>'+
307
373
  '<div class="card"><div class="card__title">Agents</div><div class="card__value">38</div><div class="card__sub">Ready</div></div>'+
308
374
  '</div>';
@@ -316,115 +382,225 @@ function renderDash(el){
316
382
  var chatReady=false;
317
383
  function renderChat(el){
318
384
  if(!chatReady||!document.getElementById('chatMessages')){
319
- 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>';
320
403
  chatReady=true;
321
404
  document.getElementById('chatSend').onclick=sendChat;
322
405
  document.getElementById('chatInput').onkeydown=function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}};
323
- 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
+ });
324
411
  setTimeout(function(){var i=document.getElementById('chatInput');if(i)i.focus()},100);
325
412
  }
326
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');}
327
430
  function renderMessages(){
328
431
  var el=document.getElementById('chatMessages');if(!el)return;
329
432
  if(chatHistory.length===0){
330
- 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>';
331
434
  return;
332
435
  }
333
- var h='';chatHistory.forEach(function(m,idx){
334
- var content = m.content || '';
335
- var isAssistant = m.role === 'assistant';
336
- var extraHtml = '';
337
-
338
- if (isAssistant) {
339
- // Handle canvas render markers
340
- var canvasMatch = content.match(/\\[CANVAS_RENDER\\]([\\s\\S]*?)\\[\\/CANVAS_RENDER\\]/);
341
- if (canvasMatch) {
342
- try { var cd = JSON.parse(canvasMatch[1]); showCanvas(cd.html, cd.title); } catch(e){}
343
- content = content.replace(/\\[CANVAS_RENDER\\][\\s\\S]*?\\[\\/CANVAS_RENDER\\]/, '').trim();
344
- }
345
- if (content.indexOf('[CANVAS_CLEAR]') !== -1) {
346
- closeCanvas();
347
- content = content.replace(/\\[CANVAS_CLEAR\\][\\s\\S]*?\\[\\/CANVAS_CLEAR\\]/, '').trim();
348
- }
349
- // Handle screenshot file markers
350
- var ssMatch = content.match(/\\[SCREENSHOT_FILE\\](.*?)\\[\\/SCREENSHOT_FILE\\]/);
351
- if (ssMatch) {
352
- var fname = ssMatch[1].split('/').pop();
353
- 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)" />';
354
- content = content.replace(/\\[SCREENSHOT_FILE\\].*?\\[\\/SCREENSHOT_FILE\\]/, '').trim();
355
- }
356
- }
357
-
358
- var bubbleContent = isAssistant ? extraHtml + esc(content).replace(/\\n/g, '<br>') : esc(content).replace(/\\n/g, '<br>');
359
-
360
- // Action buttons for each message
361
- var actions = '<div class="msg__actions" style="display:flex;gap:6px;margin-top:4px;opacity:0.3">';
362
- 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>';
363
- if (isAssistant) {
364
- 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>';
365
- } else {
366
- 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>';
367
- }
368
- actions += '</div>';
369
-
370
- h+='<div class="msg msg--'+esc(m.role)+'" onmouseenter="this.querySelector(\'.msg__actions\').style.opacity=1" onmouseleave="this.querySelector(\'.msg__actions\').style.opacity=0.3"><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>';
371
447
  });
372
448
  el.innerHTML=h;el.scrollTop=el.scrollHeight;
373
449
  }
374
- function copyMessage(idx){
375
- var m=chatHistory[idx];if(!m)return;
376
- var text=m.content.replace(/\\[SCREENSHOT_FILE\\].*?\\[\\/SCREENSHOT_FILE\\]/g,'').replace(/\\[CANVAS_RENDER\\][\\s\\S]*?\\[\\/CANVAS_RENDER\\]/g,'').trim();
377
- navigator.clipboard.writeText(text).then(function(){showToast('copy','Copied','Message copied to clipboard',2000)}).catch(function(){});
378
- }
379
- function retryMessage(idx){
380
- // Retry = re-send the user message that preceded this assistant message
381
- if(idx<1||chatHistory[idx].role!=='assistant')return;
382
- var userMsg=chatHistory[idx-1];
383
- if(!userMsg||userMsg.role!=='user')return;
384
- // Remove this assistant response and re-send
385
- chatHistory.splice(idx,1);
386
- saveChatToStorage();renderMessages();
387
- chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
388
- apiPost('/api/chat',{message:userMsg.content,history:chatHistory.slice(0,-1)}).then(function(r){
389
- chatHistory.pop();
390
- if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
391
- else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
392
- else{chatHistory.push({role:'assistant',content:'Error: no response from server'})}
393
- saveChatToStorage();renderMessages();
394
- });
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
+ }
395
477
  }
396
- function editMessage(idx){
397
- if(chatHistory[idx].role!=='user')return;
398
- var inp=document.getElementById('chatInput');if(!inp)return;
399
- inp.value=chatHistory[idx].content;
400
- inp.focus();
401
- // Remove this message and all subsequent messages
402
- chatHistory.splice(idx);
403
- 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='';
404
497
  }
498
+
405
499
  function sendChat(){
406
500
  var inp=document.getElementById('chatInput');if(!inp)return;
407
- var msg=inp.value.trim();if(!msg)return;
408
- chatHistory.push({role:'user',content:msg});
409
- inp.value='';saveChatToStorage();renderMessages();
410
- chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
411
- apiPost('/api/chat',{message:msg,history:chatHistory.slice(0,-1)}).then(function(r){
412
- chatHistory.pop();
413
- if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
414
- else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
415
- else{chatHistory.push({role:'assistant',content:'Error: no response from server'})}
416
- saveChatToStorage();renderMessages();
417
- // Refresh ALL data after any tool execution
418
- if(r&&((r.actions&&r.actions.length>0)||(r.toolResults&&r.toolResults.length>0))){
419
- calEventsCache={};
420
- contactsData=null;
421
- notesData=null;
422
- driveData=null;
423
- onedriveData=null;
424
- mstodoData=null;
425
- 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;}
426
520
  }
427
- });
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();}});
428
604
  }
429
605
 
430
606
  // ---- TASKS ----
@@ -494,17 +670,64 @@ function refreshPlan(){
494
670
 
495
671
  // ---- EMAILS ----
496
672
  function renderEmails(el){
673
+ if(!dashLoaded.emails){el.innerHTML=loadingHTML('emails');return}
497
674
  var e=dash.emails;
498
- if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No unread emails</div>';return}
499
- 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){
500
682
  var unreadStyle=x.isUnread?'border-left:3px solid var(--green);font-weight:700':'border-left:3px solid transparent;opacity:0.7';
501
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>';
502
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
+ }
503
689
  el.innerHTML=h;
504
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
+ }
505
721
  var openEmailId=null;
506
722
  function openEmail(id){
507
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
+ }
508
731
  var el=document.getElementById('content');
509
732
  el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading email...</div></div>';
510
733
  apiPost('/api/email/read',{messageId:id}).then(function(r){
@@ -1153,22 +1376,68 @@ function renderAgents(el){
1153
1376
 
1154
1377
  var filtered=agentFilter?agentsList.filter(function(a){return a.category===agentFilter}):agentsList;
1155
1378
 
1379
+ h+='<div style="margin-bottom:10px"><button class="btn btn--primary" style="font-size:11px" onclick="showCreateAgentForm()">+ Create Agent</button></div>';
1156
1380
  h+='<div class="agents-grid">';
1157
1381
  filtered.forEach(function(a){
1158
1382
  var name=a.name||a.agentName;
1159
1383
  var display=a.displayName||name;
1160
1384
  var icon=AGENT_ICONS[name.toLowerCase()]||'\\u{1F916}';
1161
1385
  var desc=AGENT_DESCRIPTIONS[name.toLowerCase()]||a.tagline||a.description||'';
1162
- 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">'+
1163
1390
  '<div class="agent-card__icon">'+icon+'</div>'+
1164
1391
  '<div class="agent-card__body"><div class="agent-card__name">'+esc(display)+'</div>'+
1165
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>'+
1166
1398
  '</div>';
1167
1399
  });
1168
1400
  h+='</div>';
1169
1401
  el.innerHTML=h;
1170
1402
  }
1171
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
+
1172
1441
  function openAgent(name,display){
1173
1442
  selectedAgent=name;
1174
1443
  attachedFileContent=null;attachedFileName=null;
@@ -1251,9 +1520,26 @@ function renderSettings(el) {
1251
1520
  ['summary-time', 'Summary Time', '18:00'],
1252
1521
  ['meeting-alert', 'Meeting Alert (minutes)', '30'],
1253
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>' +
1254
1529
  '</div>';
1255
1530
  }
1256
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
+
1257
1543
  function settingsSection(id, title, desc, fields) {
1258
1544
  var h = '<form class="card" style="margin-bottom:16px" id="settings-' + id + '" onsubmit="event.preventDefault();saveSettingsSection(\\x27' + id + '\\x27)">' +
1259
1545
  '<div class="card__title" style="color:var(--green);font-size:14px;margin-bottom:4px">' + esc(title) + '</div>' +
@@ -1474,61 +1760,6 @@ function handleDaemonEvent(msg) {
1474
1760
  showToast('plan', 'Daily Plan Ready', 'Your plan for ' + msg.data.date + ' has been generated.', 10000);
1475
1761
  if (currentView === 'plan') renderPlan(document.getElementById('content'));
1476
1762
  break;
1477
-
1478
- case 'cron_result':
1479
- showToast('cron', 'Scheduled Task', msg.data.prompt + '\\n' + (msg.data.result || '').slice(0, 100), 8000);
1480
- break;
1481
- }
1482
- }
1483
-
1484
- // ---- CANVAS PANEL ----
1485
- var canvasVisible = false;
1486
- function showCanvas(html, title) {
1487
- var panel = document.getElementById('canvasPanel');
1488
- if (!panel) {
1489
- panel = document.createElement('div');
1490
- panel.id = 'canvasPanel';
1491
- 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)';
1492
- document.body.appendChild(panel);
1493
- }
1494
- 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)">' +
1495
- '<span style="font-family:var(--mono);color:var(--green);font-size:12px">' + (title || 'Canvas') + '</span>' +
1496
- '<div><button onclick="toggleCanvasSize()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;margin-right:8px" title="Resize">&#x2922;</button>' +
1497
- '<button onclick="closeCanvas()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Close">&times;</button></div></div>';
1498
- var iframe = '<iframe id="canvasFrame" sandbox="allow-scripts allow-same-origin" style="flex:1;border:none;background:#fff;min-height:300px;width:100%"></iframe>';
1499
- panel.innerHTML = header + iframe;
1500
- panel.style.display = 'flex';
1501
- canvasVisible = true;
1502
-
1503
- // Write HTML to iframe
1504
- var frame = document.getElementById('canvasFrame');
1505
- if (frame) {
1506
- var doc = frame.contentDocument || frame.contentWindow.document;
1507
- doc.open();
1508
- doc.write(html);
1509
- doc.close();
1510
- }
1511
- }
1512
-
1513
- function closeCanvas() {
1514
- var panel = document.getElementById('canvasPanel');
1515
- if (panel) { panel.style.display = 'none'; canvasVisible = false; }
1516
- }
1517
-
1518
- function toggleCanvasSize() {
1519
- var panel = document.getElementById('canvasPanel');
1520
- if (!panel) return;
1521
- if (panel.style.width === '480px') {
1522
- panel.style.width = '80vw';
1523
- panel.style.height = '80vh';
1524
- panel.style.top = '10vh';
1525
- panel.style.right = '10vw';
1526
- } else {
1527
- panel.style.width = '480px';
1528
- panel.style.height = '';
1529
- panel.style.maxHeight = 'calc(100vh - 80px)';
1530
- panel.style.top = '60px';
1531
- panel.style.right = '12px';
1532
1763
  }
1533
1764
  }
1534
1765
 
@@ -1634,8 +1865,13 @@ init();
1634
1865
  <div class="app">
1635
1866
  <nav class="sidebar" id="sidebar">
1636
1867
  <div class="sidebar__brand">
1637
- <div class="sidebar__brand-name">NHA</div>
1638
- <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>
1639
1875
  </div>
1640
1876
  <div class="sidebar__section">
1641
1877
  <div class="sidebar__label">Overview</div>
@@ -1672,17 +1908,30 @@ init();
1672
1908
  <div class="sidebar__label">Config</div>
1673
1909
  <div class="nav-item" data-view="settings" onclick="switchView('settings')"><span class="nav-item__icon">&#9881;</span> Settings</div>
1674
1910
  </div>
1675
- <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>
1676
1918
  </nav>
1677
1919
 
1678
- <div class="header">
1679
- <button class="header__burger" onclick="toggleSidebar()">&#9776;</button>
1680
- <span class="header__title" id="headerTitle">Dashboard</span>
1681
- <span id="wsIndicator" style="color:var(--dim);font-size:8px;margin-right:4px" title="Daemon WebSocket">&#9679;</span>
1682
- <span class="header__clock" id="clock"></span>
1683
- </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>
1684
1921
 
1685
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>
1686
1935
  </div>
1687
1936
 
1688
1937
  <div class="modal-overlay" id="agentModal">