nothumanallowed 9.5.2 → 9.7.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,7 +46,10 @@ 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 removed info moved to sidebar brand */
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)}
50
53
 
51
54
  .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)}
52
55
  .sidebar--open{display:flex}
@@ -84,9 +87,8 @@ input:focus,textarea:focus{border-color:var(--green3)}
84
87
  .section-title{font-size:12px;color:var(--cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
85
88
 
86
89
  /* ---- CHAT ---- */
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}}
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)}}
90
92
  .chat__messages{flex:1;overflow-y:auto;padding-bottom:12px;-webkit-overflow-scrolling:touch}
91
93
  .chat__empty{text-align:center;padding:60px 16px;color:var(--dim)}
92
94
  .chat__empty-title{font-size:28px;color:var(--green);margin-bottom:12px}
@@ -96,30 +98,10 @@ input:focus,textarea:focus{border-color:var(--green3)}
96
98
  .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}
97
99
  .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
98
100
  .msg--thinking{color:var(--dim);font-style:italic}
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}
101
+ .chat__bar{display:flex;gap:8px;padding:10px 0 0 0;border-top:1px solid var(--border);flex-shrink:0}
118
102
  .chat__input{flex:1;resize:none;min-height:40px;max-height:100px;padding:10px 14px}
119
103
  .chat__send{background:var(--green3);color:var(--bg);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
120
104
  .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}
123
105
 
124
106
  /* ---- TASKS ---- */
125
107
  .task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
@@ -216,52 +198,11 @@ input:focus,textarea:focus{border-color:var(--green3)}
216
198
  const JS = `
217
199
  var API = '';
218
200
  var currentView = 'dashboard';
219
- var chatHistory = [];
220
- var activeConvId = null;
221
- var convList = [];
201
+ var chatHistory = (function(){try{var s=localStorage.getItem('nha_chat_history');return s?JSON.parse(s):[];}catch(e){return [];}})();
222
202
  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
- }
241
203
 
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()}
204
+ function saveChatToStorage(){try{localStorage.setItem('nha_chat_history',JSON.stringify(chatHistory.slice(-40)));}catch(e){}}
205
+ function clearChatHistory(){chatHistory=[];saveChatToStorage();renderMessages();}
265
206
  var agentsList = [];
266
207
  var selectedAgent = null;
267
208
 
@@ -278,11 +219,7 @@ function switchView(v) {
278
219
  if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
279
220
  });
280
221
  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'};
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')}}
222
+ document.getElementById('headerTitle').textContent = titles[v]||v;
286
223
  closeSidebar();
287
224
  render();
288
225
  }
@@ -314,11 +251,10 @@ function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){retur
314
251
 
315
252
  // ---- LOAD DATA ----
316
253
  function loadDash(){
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()});
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
+ });
322
258
  }
323
259
  function loadAgents(){return apiGet('/api/agents').then(function(r){agentsList=(r&&r.agents)||[]})}
324
260
  function updateBadges(){
@@ -332,7 +268,6 @@ function updateBadges(){
332
268
  // ---- HELPERS ----
333
269
  function esc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
334
270
  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>'}
336
271
 
337
272
  // ---- RENDER ----
338
273
  function render(){
@@ -361,14 +296,13 @@ function render(){
361
296
 
362
297
  // ---- DASHBOARD ----
363
298
  function renderDash(el){
364
- if(!dashLoaded.tasks&&!dashLoaded.events&&!dashLoaded.emails){el.innerHTML=loadingHTML('dashboard');return}
365
299
  var t=dash.tasks,e=dash.emails,ev=dash.events;
366
300
  var done=t.filter(function(x){return x.status==='done'}).length;
367
301
  var pend=t.length-done;
368
302
  var pct=t.length>0?Math.round(done/t.length*100):0;
369
303
  var h='<div class="dash-grid">'+
370
304
  '<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>'+
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>'+
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>'+
372
306
  '<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>'+
373
307
  '<div class="card"><div class="card__title">Agents</div><div class="card__value">38</div><div class="card__sub">Ready</div></div>'+
374
308
  '</div>';
@@ -382,225 +316,115 @@ function renderDash(el){
382
316
  var chatReady=false;
383
317
  function renderChat(el){
384
318
  if(!chatReady||!document.getElementById('chatMessages')){
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>';
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>';
403
320
  chatReady=true;
404
321
  document.getElementById('chatSend').onclick=sendChat;
405
322
  document.getElementById('chatInput').onkeydown=function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}};
406
- loadConvList().then(function(){
407
- if(!activeConvId&&convList.length>0){loadConv(convList[0].id)}
408
- else if(!activeConvId){createNewConv()}
409
- else{loadConv(activeConvId)}
410
- });
323
+ renderMessages();
411
324
  setTimeout(function(){var i=document.getElementById('chatInput');if(i)i.focus()},100);
412
325
  }
413
326
  }
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');}
430
327
  function renderMessages(){
431
328
  var el=document.getElementById('chatMessages');if(!el)return;
432
329
  if(chatHistory.length===0){
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>';
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>';
434
331
  return;
435
332
  }
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>';
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>';
447
371
  });
448
372
  el.innerHTML=h;el.scrollTop=el.scrollHeight;
449
373
  }
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
- }
477
- }
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);
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
+ });
490
395
  }
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='';
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();
497
404
  }
498
-
499
405
  function sendChat(){
500
406
  var inp=document.getElementById('chatInput');if(!inp)return;
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;}
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(){});
520
426
  }
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();}});
427
+ });
604
428
  }
605
429
 
606
430
  // ---- TASKS ----
@@ -670,64 +494,17 @@ function refreshPlan(){
670
494
 
671
495
  // ---- EMAILS ----
672
496
  function renderEmails(el){
673
- if(!dashLoaded.emails){el.innerHTML=loadingHTML('emails');return}
674
497
  var e=dash.emails;
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){
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){
682
500
  var unreadStyle=x.isUnread?'border-left:3px solid var(--green);font-weight:700':'border-left:3px solid transparent;opacity:0.7';
683
501
  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>';
684
502
  });
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
- }
689
503
  el.innerHTML=h;
690
504
  }
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
- }
721
505
  var openEmailId=null;
722
506
  function openEmail(id){
723
507
  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
- }
731
508
  var el=document.getElementById('content');
732
509
  el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading email...</div></div>';
733
510
  apiPost('/api/email/read',{messageId:id}).then(function(r){
@@ -1376,68 +1153,22 @@ function renderAgents(el){
1376
1153
 
1377
1154
  var filtered=agentFilter?agentsList.filter(function(a){return a.category===agentFilter}):agentsList;
1378
1155
 
1379
- h+='<div style="margin-bottom:10px"><button class="btn btn--primary" style="font-size:11px" onclick="showCreateAgentForm()">+ Create Agent</button></div>';
1380
1156
  h+='<div class="agents-grid">';
1381
1157
  filtered.forEach(function(a){
1382
1158
  var name=a.name||a.agentName;
1383
1159
  var display=a.displayName||name;
1384
1160
  var icon=AGENT_ICONS[name.toLowerCase()]||'\\u{1F916}';
1385
1161
  var desc=AGENT_DESCRIPTIONS[name.toLowerCase()]||a.tagline||a.description||'';
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">'+
1162
+ h+='<div class="card agent-card" onclick="openAgent(\\''+esc(name)+'\\',\\''+esc(display)+'\\')">'+
1390
1163
  '<div class="agent-card__icon">'+icon+'</div>'+
1391
1164
  '<div class="agent-card__body"><div class="agent-card__name">'+esc(display)+'</div>'+
1392
1165
  '<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>'+
1398
1166
  '</div>';
1399
1167
  });
1400
1168
  h+='</div>';
1401
1169
  el.innerHTML=h;
1402
1170
  }
1403
1171
  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
-
1441
1172
  function openAgent(name,display){
1442
1173
  selectedAgent=name;
1443
1174
  attachedFileContent=null;attachedFileName=null;
@@ -1520,26 +1251,9 @@ function renderSettings(el) {
1520
1251
  ['summary-time', 'Summary Time', '18:00'],
1521
1252
  ['meeting-alert', 'Meeting Alert (minutes)', '30'],
1522
1253
  ]) +
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>' +
1529
1254
  '</div>';
1530
1255
  }
1531
1256
 
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
-
1543
1257
  function settingsSection(id, title, desc, fields) {
1544
1258
  var h = '<form class="card" style="margin-bottom:16px" id="settings-' + id + '" onsubmit="event.preventDefault();saveSettingsSection(\\x27' + id + '\\x27)">' +
1545
1259
  '<div class="card__title" style="color:var(--green);font-size:14px;margin-bottom:4px">' + esc(title) + '</div>' +
@@ -1760,6 +1474,61 @@ function handleDaemonEvent(msg) {
1760
1474
  showToast('plan', 'Daily Plan Ready', 'Your plan for ' + msg.data.date + ' has been generated.', 10000);
1761
1475
  if (currentView === 'plan') renderPlan(document.getElementById('content'));
1762
1476
  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';
1763
1532
  }
1764
1533
  }
1765
1534
 
@@ -1865,13 +1634,8 @@ init();
1865
1634
  <div class="app">
1866
1635
  <nav class="sidebar" id="sidebar">
1867
1636
  <div class="sidebar__brand">
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>
1637
+ <div class="sidebar__brand-name">NHA</div>
1638
+ <div class="sidebar__brand-sub">Operations Console</div>
1875
1639
  </div>
1876
1640
  <div class="sidebar__section">
1877
1641
  <div class="sidebar__label">Overview</div>
@@ -1908,30 +1672,17 @@ init();
1908
1672
  <div class="sidebar__label">Config</div>
1909
1673
  <div class="nav-item" data-view="settings" onclick="switchView('settings')"><span class="nav-item__icon">&#9881;</span> Settings</div>
1910
1674
  </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>
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>
1918
1676
  </nav>
1919
1677
 
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>
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>
1921
1684
 
1922
1685
  <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>
1935
1686
  </div>
1936
1687
 
1937
1688
  <div class="modal-overlay" id="agentModal">