nothumanallowed 12.7.0 → 13.2.13

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.
@@ -4,263 +4,8 @@
4
4
 
5
5
  import { VERSION } from '../constants.mjs';
6
6
 
7
- export function getHTML(port) {
8
- const ts = Date.now();
9
-
10
- // CSS as a clean block — no template literal nesting issues
11
- const CSS = `
12
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
- :root{
14
- --bg:#0a0a0a;--bg2:#111;--bg3:#1a1a2e;--bg4:#222;
15
- --green:#a5b4fc;--green2:#818cf8;--green3:#6366f1;--greendim:#12121e;
16
- --cyan:#38bdf8;--amber:#fbbf24;--red:#ef4444;
17
- --text:#e4e4e7;--dim:#9ca3af;--bright:#fff;
18
- --border:#1e1e2e;--border2:#3f3f5e;
19
- --font:'JetBrains Mono','Fira Code','SF Mono','Consolas',monospace;
20
- --r:6px;
21
- }
22
- html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;line-height:1.5}
23
- a{color:var(--cyan);text-decoration:none}
24
- button{font-family:var(--font);cursor:pointer;border:none;outline:none}
25
- input,textarea{font-family:var(--font);background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:var(--r);outline:none;font-size:13px}
26
- input:focus,textarea:focus{border-color:var(--green3)}
27
- ::-webkit-scrollbar{width:6px}
28
- ::-webkit-scrollbar-track{background:var(--bg)}
29
- ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
30
-
31
- /* ---- TASKS ---- */
32
- .task-bar{display:flex;gap:8px;margin-bottom:16px}
33
- .task-bar input{flex:1;font-size:13px;padding:10px 14px}
34
- .task-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--r);font-size:12px}
35
- .task-bar button{background:var(--green3);color:var(--bg);padding:8px 20px;border-radius:var(--r);font-weight:700;font-size:13px}
36
- .task{display:flex;align-items:center;gap:10px;padding:12px;cursor:pointer}
37
- .task--done{opacity:0.5}
38
- .task__check{width:24px;height:24px;border:2px solid var(--border2);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--green);cursor:pointer;flex-shrink:0}
39
- .task__check--done{background:var(--green3);border-color:var(--green)}
40
- .task__desc{flex:1;font-size:13px}
41
- .task__priority{font-size:10px;padding:2px 8px;border-radius:4px;text-transform:uppercase}
42
- .task__priority--critical{background:var(--red);color:var(--bright)}
43
- .task__priority--high{background:#ff6d00;color:var(--bright)}
44
- .task__priority--medium{background:var(--amber);color:var(--bg)}
45
- .task__priority--low{background:var(--border2);color:var(--dim)}
46
-
47
- /* ---- LAYOUT: mobile-first ---- */
48
- .app{display:flex;flex-direction:column;height:100vh;height:100dvh}
49
- /* header removed — info moved to sidebar brand */
50
-
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)}
52
- .sidebar--open{display:flex}
53
- .sidebar__overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:150}
54
- .sidebar__overlay--open{display:block}
55
- .sidebar__brand{padding:16px;border-bottom:1px solid var(--border)}
56
- .sidebar__brand-name{font-size:16px;color:var(--green);font-weight:700;letter-spacing:2px}
57
- .sidebar__brand-sub{font-size:10px;color:var(--dim);margin-top:2px}
58
- .sidebar__section{padding:12px 0}
59
- .sidebar__label{padding:0 16px;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--dim);margin-bottom:4px}
60
- .nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;color:var(--dim);cursor:pointer;font-size:12px;border-left:2px solid transparent}
61
- .nav-item:hover,.nav-item--active{color:var(--bright);background:var(--bg3)}
62
- .nav-item--active{border-left-color:var(--green);color:var(--green)}
63
- .nav-item__icon{width:18px;text-align:center}
64
- .nav-item__badge{background:var(--red);color:var(--bright);font-size:9px;padding:1px 5px;border-radius:8px;margin-left:auto}
65
-
66
- .content{flex:1;overflow-y:auto;padding:16px;-webkit-overflow-scrolling:touch}
67
-
68
- /* Mobile burger button */
69
- #mobileBurger{display:block}
70
- .sidebar__close{position:absolute;top:12px;right:12px;background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer;padding:4px 8px;z-index:10;line-height:1}
71
- .sidebar__close:hover{color:var(--bright)}
72
- .sidebar__brand{position:relative}
73
-
74
- /* ---- DESKTOP: sidebar always visible ---- */
75
- @media(min-width:901px){
76
- .app{flex-direction:row}
77
- .header__burger{display:none}
78
- #mobileBurger{display:none!important}
79
- .sidebar__close{display:none}
80
- .sidebar{display:flex!important;position:static;width:220px;min-width:220px;height:auto;box-shadow:none}
81
- .sidebar__overlay{display:none!important}
82
- .content{padding:20px}
83
- }
84
-
85
- /* ---- CARDS ---- */
86
- .card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:10px}
87
- .card__title{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin-bottom:6px}
88
- .card__value{font-size:22px;font-weight:700;color:var(--green)}
89
- .card__sub{font-size:11px;color:var(--dim);margin-top:3px}
90
- .dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
91
- @media(min-width:901px){.dash-grid{grid-template-columns:repeat(4,1fr)}}
92
- .section-title{font-size:12px;color:var(--cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
93
-
94
- /* ---- CHAT ---- */
95
- .content--chat{overflow:hidden!important;padding:0!important;display:flex;flex-direction:column}
96
- .chat{display:flex;flex-direction:column;flex:1;min-height:0;padding:16px;padding-bottom:0}
97
- @media(min-width:901px){.content--chat{padding:0!important}}
98
- .chat__messages{flex:1;overflow-y:auto;padding-bottom:12px;-webkit-overflow-scrolling:touch}
99
- .chat__empty{text-align:center;padding:60px 16px;color:var(--dim)}
100
- .chat__empty-title{font-size:28px;color:var(--green);margin-bottom:12px}
101
- .chat__empty-hint{font-size:11px;margin-top:12px}
102
- .msg{margin-bottom:12px}
103
- .msg--user .msg__bubble{background:var(--bg3);border:1px solid var(--border2);border-radius:8px 8px 2px 8px;padding:10px 14px;max-width:85%;margin-left:auto;color:var(--bright)}
104
- .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;line-height:1.5}
105
- .msg--assistant .msg__bubble img{max-width:100%;border-radius:8px;margin:8px 0;border:1px solid rgba(0,255,65,0.2)}
106
- .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
107
- .msg__actions{display:flex;gap:6px;margin-top:4px;opacity:0.4;transition:opacity 0.2s}
108
- .msg:hover .msg__actions{opacity:1}
109
- .msg__actions button{background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;font-family:var(--mono);padding:2px 4px}
110
- .msg__actions button:hover{color:var(--green)}
111
- #canvasPanel{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:none;flex-direction:column;box-shadow:0 0 30px rgba(0,255,65,0.1)}
112
- #canvasPanel.open{display:flex}
113
- #canvasPanel .cvs-header{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)}
114
- #canvasPanel .cvs-header span{font-family:var(--mono);color:var(--green);font-size:12px}
115
- #canvasPanel .cvs-header button{background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;margin-left:8px}
116
- #canvasPanel iframe{flex:1;border:none;background:#fff;min-height:350px;width:100%}
117
- .msg--thinking{color:var(--dim);font-style:italic}
118
- .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)}
119
- .tool-indicator--browser{border-color:#9c27b0;color:#ce93d8}
120
- .tool-indicator--web{border-color:var(--cyan);color:var(--cyan)}
121
- .tool-indicator--email{border-color:var(--green3);color:var(--green)}
122
- .screenshot-preview{max-width:100%;border-radius:var(--r);margin:8px 0;border:1px solid var(--border)}
123
- .inline-card{margin:12px 0;padding:0;border-radius:10px;border:1px solid var(--border);overflow:hidden;background:var(--bg2)}
124
- .inline-card iframe{width:100%;height:280px;border:none;border-radius:0 0 10px 10px}
125
- .inline-card a{color:var(--cyan);text-decoration:none}
126
- .inline-card a:hover{text-decoration:underline}
127
- .inline-browser{margin:12px 0;border-radius:10px;border:1px solid var(--green3);overflow:hidden;background:#000}
128
- .inline-browser-bar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--bg);border-bottom:1px solid var(--border)}
129
- .inline-browser-dot{width:8px;height:8px;border-radius:50%;background:var(--border2)}
130
- .inline-browser-url{font-family:var(--mono);font-size:10px;color:var(--dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
131
- .inline-browser img{width:100%;display:block}
132
- .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}
133
- .browser-viewer--open{display:block}
134
- .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}
135
- .browser-viewer__dot{width:6px;height:6px;border-radius:50%;background:#9c27b0;animation:bvpulse 1.5s infinite}
136
- @keyframes bvpulse{0%,100%{opacity:1}50%{opacity:.3}}
137
- .browser-viewer__title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
138
- .browser-viewer__close{background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0 4px}
139
- .browser-viewer__close:hover{color:#fff}
140
- .browser-viewer__frame{width:100%;aspect-ratio:16/9;background:#000;display:flex;align-items:center;justify-content:center}
141
- .browser-viewer__frame img{width:100%;height:100%;object-fit:contain}
142
- .browser-viewer__status{padding:4px 10px;font-size:9px;color:var(--dim);border-top:1px solid var(--border)}
143
- @media(max-width:600px){.browser-viewer{width:calc(100vw - 24px);top:8px;left:8px}}
144
- @media(min-width:901px){.browser-viewer{left:232px}}
145
- .chat__bar{display:flex;flex-wrap:wrap;gap:6px;padding:10px 0 12px 0;border-top:1px solid var(--border);flex-shrink:0;align-items:flex-end}
146
- .chat__bar-tools{display:flex;gap:4px;align-items:center;width:100%}
147
- .chat__input{flex:1;resize:none;min-height:44px;max-height:120px;padding:10px 14px;font-size:14px}
148
- .chat__send{background:var(--green3);color:var(--bg);padding:10px 20px;border-radius:var(--r);font-weight:700;font-size:14px;white-space:nowrap}
149
- .chat__send:disabled{opacity:.4}
150
- .chat__stop{white-space:nowrap}
151
-
152
- /* ---- MOBILE TOUCH (Termux / small screens) ---- */
153
- @media(max-width:600px){
154
- .msg--user .msg__bubble,.msg--assistant .msg__bubble{font-size:14px;padding:12px 14px;max-width:94%;line-height:1.55}
155
- .msg__label{font-size:11px}
156
- .chat__bar{flex-wrap:wrap;gap:6px;padding:8px 4px 10px 4px}
157
- .chat__bar-tools{flex-wrap:wrap;gap:4px}
158
- .chat__input{min-height:48px;font-size:15px;padding:12px 14px;width:100%;flex-basis:100%}
159
- .chat__send{padding:12px 20px;font-size:15px;flex-grow:1}
160
- .chat__stop{flex-grow:1}
161
- .chat__empty-title{font-size:22px}
162
- .header{padding:10px 12px}
163
- .header__title{font-size:15px}
164
- .content{padding:10px}
165
- /* Conversation sidebar as overlay on mobile, not side panel */
166
- #convSidebar{position:fixed!important;top:0!important;left:0!important;height:100dvh!important;width:260px!important;box-shadow:4px 0 20px rgba(0,0,0,0.8)!important;z-index:250!important}
167
- }
168
- .chat__stop{background:var(--red);color:var(--bright);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px;display:none}
169
- .chat__stop--visible{display:block}
170
-
171
- /* ---- TASKS ---- */
172
- .task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
173
- .task-bar input{flex:1;min-width:150px}
174
- .task-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--r);font-size:12px}
175
- .task-bar button{background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
176
- .task{display:flex;align-items:center;gap:10px;padding:10px 14px}
177
- .task--done{opacity:.5}
178
- .task__check{width:18px;height:18px;border:2px solid var(--border2);border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;color:var(--green)}
179
- .task__check--done{background:var(--green3);border-color:var(--green)}
180
- .task__desc{flex:1;min-width:0}
181
- .task__priority{font-size:9px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
182
- .task__priority--high,.task__priority--critical{background:var(--red);color:var(--bright)}
183
- .task__priority--medium{background:var(--amber);color:var(--bg)}
184
- .task__priority--low{background:var(--border2);color:var(--dim)}
185
-
186
- /* ---- PLAN ---- */
187
- .plan-summary{padding:16px;border-left:3px solid var(--green);background:var(--bg2);border-radius:0 var(--r) var(--r) 0;margin-bottom:16px;line-height:1.6}
188
- .plan-action{display:flex;gap:10px;align-items:baseline;padding:6px 0}
189
- .plan-action__time{color:var(--amber);min-width:50px;font-weight:600}
190
- .plan-action__text{flex:1}
191
- .plan-action__priority{font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700}
192
-
193
- /* ---- EMAILS ---- */
194
- .email{padding:12px 14px}
195
- .email__header{display:flex;justify-content:space-between;gap:8px}
196
- .email__from{color:var(--cyan);font-weight:600;font-size:12px}
197
- .email__date{color:var(--dim);font-size:10px;white-space:nowrap}
198
- .email__subject{color:var(--bright);margin-top:3px}
199
- .email__snippet{color:var(--dim);font-size:11px;margin-top:3px}
200
-
201
- /* ---- CALENDAR ---- */
202
- .event{display:flex;gap:12px;align-items:center;padding:10px 14px}
203
- .event__time{color:var(--amber);font-weight:600;min-width:100px;white-space:nowrap;font-size:12px}
204
- .event__title{color:var(--bright);flex:1}
205
- .event__location{color:var(--dim);font-size:11px}
206
-
207
- /* ---- AGENTS ---- */
208
- .agents-grid{display:grid;grid-template-columns:1fr;gap:6px}
209
- @media(min-width:500px){.agents-grid{grid-template-columns:1fr 1fr}}
210
- @media(min-width:901px){.agents-grid{grid-template-columns:1fr 1fr 1fr}}
211
- .agent-card{padding:8px 10px;text-align:left;cursor:pointer;transition:border-color .15s;display:flex;align-items:center;gap:10px}
212
- .agent-card:hover{border-color:var(--green3)}
213
- .agent-card__icon{font-size:18px;flex-shrink:0}
214
- .agent-card__body{min-width:0;flex:1}
215
- .agent-card__name{color:var(--green);font-weight:700;font-size:11px}
216
- .agent-card__tagline{font-size:9px;color:var(--text);line-height:1.3}
217
- .agent-card__cat{display:none}
218
-
219
- /* ---- MODAL ---- */
220
- .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:300;align-items:center;justify-content:center}
221
- .modal-overlay--open{display:flex}
222
- .modal{background:var(--bg2);border:1px solid var(--border2);border-radius:8px;width:92%;max-width:540px;max-height:90vh;display:flex;flex-direction:column}
223
- .modal__header{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--border)}
224
- .modal__header h2{font-size:16px;color:var(--green)}
225
- .modal__close{background:none;color:var(--dim);font-size:24px;padding:0 4px}
226
- .modal__body{padding:16px;overflow-y:auto;flex:1}
227
- .modal__body textarea{width:100%;min-height:80px;margin-bottom:10px}
228
- .modal__response{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:12px;white-space:pre-wrap;word-wrap:break-word;max-height:300px;overflow-y:auto;font-size:12px}
229
- .modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--border)}
230
- .btn{padding:8px 16px;border-radius:var(--r);font-size:12px;font-weight:600}
231
- .btn--primary{background:var(--green3);color:var(--bg)}
232
- .btn--secondary{background:var(--bg3);color:var(--dim);border:1px solid var(--border)}
233
-
234
- /* ---- SPINNER ---- */
235
- .spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin .6s linear infinite;margin:0 auto 12px}
236
- @keyframes spin{to{transform:rotate(360deg)}}
237
-
238
- /* ---- TOASTS (real-time notifications) ---- */
239
- .toast-container{position:fixed;top:16px;right:16px;z-index:500;display:flex;flex-direction:column;gap:8px;pointer-events:none}
240
- .toast{background:var(--bg3);border:1px solid var(--green);border-radius:8px;padding:12px 16px;max-width:320px;animation:toastIn .3s;pointer-events:auto;cursor:pointer;box-shadow:0 4px 20px rgba(0,0,0,0.5)}
241
- .toast--email{border-color:var(--cyan)}
242
- .toast--meeting{border-color:var(--amber)}
243
- .toast--security{border-color:var(--red)}
244
- .toast--plan{border-color:var(--green)}
245
- .toast__title{font-size:11px;font-weight:700;margin-bottom:3px}
246
- .toast--email .toast__title{color:var(--cyan)}
247
- .toast--meeting .toast__title{color:var(--amber)}
248
- .toast--security .toast__title{color:var(--red)}
249
- .toast--plan .toast__title{color:var(--green)}
250
- .toast__body{font-size:11px;color:var(--text);line-height:1.4}
251
- .toast--fadeout{animation:toastOut .3s forwards}
252
- @keyframes toastIn{from{transform:translateX(100%);opacity:0}to{transform:none;opacity:1}}
253
- @keyframes toastOut{from{opacity:1}to{opacity:0;transform:translateX(40px)}}
254
-
255
- /* ---- VOICE MIC BUTTON (in chat bar) ---- */
256
- .chat__mic{background:var(--bg3);color:var(--green);border:1px solid var(--border2);width:40px;height:40px;border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer;flex-shrink:0;transition:all .2s}
257
- .chat__mic:hover{border-color:var(--green3);background:var(--greendim)}
258
- .chat__mic--recording{border-color:var(--red);color:var(--red);background:rgba(255,23,68,0.1);animation:micPulse 1.5s ease-in-out infinite}
259
- @keyframes micPulse{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 6px rgba(255,23,68,0.2)}}
260
- `;
261
-
262
- // JS as clean block
263
- const JS = `
7
+ // JS bundle — served as /nha-ui.js to avoid browser SyntaxError from large inline scripts
8
+ const JS = `
264
9
  var API = '';
265
10
  var currentView = 'dashboard';
266
11
  var chatHistory = [];
@@ -271,6 +16,117 @@ var dashLoaded = {emails:false,events:false,tasks:false,contacts:false,notes:fal
271
16
  var chatStreaming = false;
272
17
  var chatAbortController = null;
273
18
 
19
+ // ---- MARKDOWN RENDERER ----
20
+ // Minimal inline markdown → HTML, no deps, safe (no innerHTML injection)
21
+ function renderMd(raw) {
22
+ if (!raw) return '';
23
+ var s = raw;
24
+ // Escape HTML first (safe base)
25
+ s = s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
26
+ // Fenced code blocks (triple-backtick) — use RegExp() to avoid backtick in template literal
27
+ var BT = String.fromCharCode(96);
28
+ var reTriple = new RegExp(BT+BT+BT+'[a-z]*[\\s\\S]*?'+BT+BT+BT,'g');
29
+ var reSingle = new RegExp(BT+'([^'+BT+']{1,200})'+BT,'g');
30
+ s = s.replace(reTriple, function(m){
31
+ var inner=m.replace(new RegExp(BT+BT+BT+'[a-z]*'),'').replace(new RegExp(BT+BT+BT),'');
32
+ return '<pre class="md-code"><code>'+inner+'</code></pre>';
33
+ });
34
+ // Inline code (single backtick)
35
+ s = s.replace(reSingle,'<code class="md-inline-code">$1</code>');
36
+ // Bold and italic — use [*] to avoid both /* */ comment ambiguity and \* escape issues
37
+ s = s.replace(new RegExp('[*][*]([^*]+)[*][*]','g'),'<strong>$1</strong>');
38
+ s = s.replace(new RegExp('[*]([^*]+)[*]','g'),'<em>$1</em>');
39
+ // Strikethrough ~~text~~
40
+ s = s.replace(/~~([^~]+)~~/g,'<del>$1</del>');
41
+ // Split into lines for block-level parsing — use fromCharCode to avoid template literal newline
42
+ var NL = String.fromCharCode(10);
43
+ var lines = s.split(NL);
44
+ var out = [];
45
+ var inUl = false, inOl = false, inPre = false;
46
+ for (var i = 0; i < lines.length; i++) {
47
+ var l = lines[i];
48
+ // Already inside pre block
49
+ if (l.indexOf('<pre class="md-code">') !== -1) { inPre = true; }
50
+ if (inPre) { out.push(l); if (l.indexOf('</pre>') !== -1) inPre = false; continue; }
51
+ // H1-H3
52
+ var hm = l.match(/^(#{1,3}) (.+)/);
53
+ if (hm) {
54
+ if (inUl) { out.push('</ul>'); inUl = false; }
55
+ if (inOl) { out.push('</ol>'); inOl = false; }
56
+ var hl = hm[1].length;
57
+ out.push('<h'+hl+' class="md-h'+hl+'">'+hm[2]+'</h'+hl+'>');
58
+ continue;
59
+ }
60
+ // Horizontal rule ---
61
+ if (/^---+$/.test(l.trim())) {
62
+ out.push('<hr class="md-hr">');
63
+ continue;
64
+ }
65
+ // Unordered list: - item or * item
66
+ var ulm = l.match(/^(\s*)[*\-] (.+)/);
67
+ if (ulm) {
68
+ if (inOl) { out.push('</ol>'); inOl = false; }
69
+ if (!inUl) { out.push('<ul class="md-ul">'); inUl = true; }
70
+ out.push('<li>'+ulm[2]+'</li>');
71
+ continue;
72
+ }
73
+ // Ordered list: 1. item
74
+ var olm = l.match(/^(\s*)\d+\. (.+)/);
75
+ if (olm) {
76
+ if (inUl) { out.push('</ul>'); inUl = false; }
77
+ if (!inOl) { out.push('<ol class="md-ol">'); inOl = true; }
78
+ out.push('<li>'+olm[2]+'</li>');
79
+ continue;
80
+ }
81
+ // Close lists on blank or non-list line
82
+ if (inUl) { out.push('</ul>'); inUl = false; }
83
+ if (inOl) { out.push('</ol>'); inOl = false; }
84
+ // Blockquote > text
85
+ var bqm = l.match(/^&gt; (.+)/);
86
+ if (bqm) { out.push('<blockquote class="md-bq">'+bqm[1]+'</blockquote>'); continue; }
87
+ // Blank line → paragraph break
88
+ if (l.trim() === '') { out.push('<div class="md-spacer"></div>'); continue; }
89
+ // Regular paragraph line
90
+ out.push('<p class="md-p">'+l+'</p>');
91
+ }
92
+ if (inUl) out.push('</ul>');
93
+ if (inOl) out.push('</ol>');
94
+ return out.join('');
95
+ }
96
+
97
+ // ---- DRAG-TO-MOVE for floating panels ----
98
+ function makeDraggable(el, handleSelector) {
99
+ var handle = handleSelector ? el.querySelector(handleSelector) : el;
100
+ if (!handle) handle = el;
101
+ var ox=0,oy=0,sx=0,sy=0,dragging=false;
102
+ handle.style.cursor='grab';
103
+ handle.addEventListener('mousedown', function(e) {
104
+ if (e.button !== 0) return;
105
+ dragging = true;
106
+ sx = e.clientX; sy = e.clientY;
107
+ var rect = el.getBoundingClientRect();
108
+ // Switch from CSS right/top to explicit left/top
109
+ el.style.right = 'auto';
110
+ el.style.left = rect.left + 'px';
111
+ el.style.top = rect.top + 'px';
112
+ ox = rect.left; oy = rect.top;
113
+ handle.style.cursor='grabbing';
114
+ e.preventDefault();
115
+ });
116
+ document.addEventListener('mousemove', function(e) {
117
+ if (!dragging) return;
118
+ var dx = e.clientX - sx, dy = e.clientY - sy;
119
+ var nx = ox+dx, ny = oy+dy;
120
+ // Clamp to viewport
121
+ nx = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, nx));
122
+ ny = Math.max(0, Math.min(window.innerHeight - 40, ny));
123
+ el.style.left = nx+'px'; el.style.top = ny+'px';
124
+ });
125
+ document.addEventListener('mouseup', function() {
126
+ if (dragging) { dragging=false; handle.style.cursor='grab'; }
127
+ });
128
+ }
129
+
274
130
  function endStreaming(){
275
131
  chatStreaming=false;chatAbortController=null;
276
132
  var stopBtn=document.getElementById('chatStop');if(stopBtn)stopBtn.classList.remove('chat__stop--visible');
@@ -314,6 +170,19 @@ function closeBrowserViewer(){
314
170
  var v=document.getElementById('browserViewer');if(v)v.classList.remove('browser-viewer--open');
315
171
  }
316
172
 
173
+ function openLightbox(src){
174
+ var o=document.getElementById('lightboxOverlay');
175
+ var img=document.getElementById('lightboxImg');
176
+ if(!o||!img||!src)return;
177
+ img.src=src;
178
+ o.classList.add('lightbox-overlay--open');
179
+ document.addEventListener('keydown',function handler(e){if(e.key==='Escape'){closeLightbox();document.removeEventListener('keydown',handler);}});
180
+ }
181
+ function closeLightbox(){
182
+ var o=document.getElementById('lightboxOverlay');
183
+ if(o)o.classList.remove('lightbox-overlay--open');
184
+ }
185
+
317
186
  function loadConvList(){return apiGet('/api/conversations').then(function(r){convList=(r&&r.conversations)||[];renderConvSidebar();})}
318
187
  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();onConversationSwitch();}})}
319
188
  function createNewConv(){return apiPost('/api/conversations',{}).then(function(r){if(r&&r.conversation){activeConvId=r.conversation.id;chatHistory=[];renderMessages();loadConvList();}})}
@@ -321,6 +190,7 @@ function deleteConv(id){return fetch(API+'/api/conversations/'+id,{method:'DELET
321
190
  function clearChatHistory(){createNewConv()}
322
191
  var agentsList = [];
323
192
  var selectedAgent = null;
193
+ var agentChatHistory = []; // [{role:'user'|'agent', text:'...'}]
324
194
 
325
195
  // ---- NAV ----
326
196
  function switchView(v) {
@@ -334,13 +204,15 @@ function switchView(v) {
334
204
  document.querySelectorAll('.nav-item').forEach(function(el){
335
205
  if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
336
206
  });
337
- 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'};
207
+ 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',studio:'Studio',settings:'Settings'};
338
208
  var spt=document.getElementById('sidebarPageTitle');
339
209
  if(spt)spt.textContent=titles[v]||v;
340
210
  // Toggle content--chat class for proper chat layout (no overflow, flex column)
341
211
  var ct=document.getElementById('content');
342
212
  if(ct){if(v==='chat'){ct.classList.add('content--chat')}else{ct.classList.remove('content--chat')}}
343
213
  closeSidebar();
214
+ // Auto-close floating panels when leaving chat/studio
215
+ if(v!=='chat'&&v!=='studio'){closeBrowserViewer();closeCanvas();}
344
216
  render();
345
217
  }
346
218
  function openSidebar() {
@@ -371,7 +243,7 @@ function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){retur
371
243
 
372
244
  // ---- LOAD DATA ----
373
245
  function loadDash(){
374
- // Load each API independentlyrender as each arrives (emails are slow)
246
+ // Load each API independently - render as each arrives (emails are slow)
375
247
  apiGet('/api/status').then(function(r){dash.status=r;render()});
376
248
  apiGet('/api/tasks').then(function(r){dash.tasks=(r&&r.tasks)||[];dashLoaded.tasks=true;updateBadges();render()});
377
249
  apiGet('/api/calendar').then(function(r){dash.events=(r&&r.events)||[];dashLoaded.events=true;updateBadges();render()});
@@ -412,6 +284,7 @@ function render(){
412
284
  case 'slack':renderSlack(el);break;
413
285
  case 'birthdays':renderBirthdays(el);break;
414
286
  case 'agents':renderAgents(el);break;
287
+ case 'studio':renderStudio(el);break;
415
288
  case 'collab':renderCollab(el);break;
416
289
  case 'settings':renderSettings(el);break;
417
290
  }
@@ -504,7 +377,7 @@ function exportConvMd(){if(!activeConvId)return;window.open(API+'/api/conversati
504
377
  function renderMessages(){
505
378
  var el=document.getElementById('chatMessages');if(!el)return;
506
379
  if(chatHistory.length===0){
507
- el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations AssistantStreaming + 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>';
380
+ 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>';
508
381
  return;
509
382
  }
510
383
  var h='';chatHistory.forEach(function(m,mi){
@@ -518,18 +391,19 @@ function renderMessages(){
518
391
  var cm=raw.match(/\\[CANVAS_RENDER\\]([\\s\\S]*?)\\[\\/CANVAS_RENDER\\]/);
519
392
  if(cm){try{var cd=JSON.parse(cm[1]);showCanvas(cd.html,cd.title);}catch(e){} raw=raw.replace(/\\[CANVAS_RENDER\\][\\s\\S]*?\\[\\/CANVAS_RENDER\\]/,'').trim();}
520
393
  if(raw.indexOf('[CANVAS_CLEAR]')!==-1){closeCanvas();raw=raw.replace(/\\[CANVAS_CLEAR\\][\\s\\S]*?\\[\\/CANVAS_CLEAR\\]/,'').trim();}
521
- // Inline cardsrendered as embedded HTML inside the message
394
+ // Inline cards - rendered as embedded HTML inside the message
522
395
  raw=raw.replace(/\\[INLINE_CARD\\]([\\s\\S]*?)\\[\\/INLINE_CARD\\]/g,function(_,html){return '<div class="inline-card">'+html+'</div>';});
523
- // Inline browser framerendered as embedded image inside the message
524
- raw=raw.replace(/\\[INLINE_BROWSER\\]([^|]+)\\|([^\\]]+)\\[\\/INLINE_BROWSER\\]/g,function(_,file,url){return '<div class="inline-browser"><div class="inline-browser-bar"><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-url">'+esc(url)+'</span></div><img src="/api/screenshots/'+esc(file)+'" alt="'+esc(url)+'"></div>';});
396
+ // Inline browser frame - rendered as embedded image inside the message
397
+ raw=raw.replace(/\\[INLINE_BROWSER\\]([^|]+)\\|([^\\]]+)\\[\\/INLINE_BROWSER\\]/g,function(_,file,url){return '<div class="inline-browser"><div class="inline-browser-bar"><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-url">'+esc(url)+'</span></div><img src="/api/screenshots/'+esc(file)+'" alt="'+esc(url)+'" onclick="openLightbox(this.src)" style="cursor:zoom-in"></div>';});
525
398
  // Handle screenshot file markers
526
399
  var sm=raw.match(/\\[SCREENSHOT_FILE\\](.*?)\\[\\/SCREENSHOT_FILE\\]/);
527
400
  if(sm){var fn=sm[1].split('/').pop();raw=raw.replace(/\\[SCREENSHOT_FILE\\].*?\\[\\/SCREENSHOT_FILE\\]/,'');raw='![Screenshot](/api/screenshots/'+fn+')\\n'+raw;}
528
401
  }
529
402
  var imgs=[];var idx=0;
530
403
  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;});
531
- var content=esc(safe);
532
- 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+'">');}
404
+ // Assistant messages get markdown rendering; user messages get plain escape
405
+ var content=isA?renderMd(safe):esc(safe);
406
+ 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+'" onclick="openLightbox(this.src)">');}
533
407
  // Action buttons + fork navigation
534
408
  var acts='<div class="msg__actions">';
535
409
  acts+='<button onclick="copyMsg('+mi+')">Copy</button>';
@@ -540,9 +414,10 @@ function renderMessages(){
540
414
  acts+='</div>';
541
415
  var inlineBlock='';
542
416
  if(isA&&m.inlineHtml){
543
- inlineBlock=m.inlineHtml.replace(/\\[INLINE_CARD\\]([\\s\\S]*?)\\[\\/INLINE_CARD\\]/g,function(_,htm){return '<div class="inline-card">'+htm+'</div>';}).replace(/\\[INLINE_BROWSER\\]([^|]+)\\|([^\\]]+)\\[\\/INLINE_BROWSER\\]/g,function(_,file,url){return '<div class="inline-browser"><div class="inline-browser-bar"><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-url">'+esc(url)+'</span></div><img src="/api/screenshots/'+esc(file)+'" alt="'+esc(url)+'"></div>';});
417
+ inlineBlock=m.inlineHtml.replace(/\\[INLINE_CARD\\]([\\s\\S]*?)\\[\\/INLINE_CARD\\]/g,function(_,htm){return '<div class="inline-card">'+htm+'</div>';}).replace(/\\[INLINE_BROWSER\\]([^|]+)\\|([^\\]]+)\\[\\/INLINE_BROWSER\\]/g,function(_,file,url){return '<div class="inline-browser"><div class="inline-browser-bar"><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-dot"></span><span class="inline-browser-url">'+esc(url)+'</span></div><img src="/api/screenshots/'+esc(file)+'" alt="'+esc(url)+'" onclick="openLightbox(this.src)" style="cursor:zoom-in"></div>';});
544
418
  }
545
- 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>'+inlineBlock+acts+'</div>';
419
+ var bubbleCls=isA?'msg__bubble md-body':'msg__bubble';
420
+ h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="'+bubbleCls+'">'+content+'</div>'+inlineBlock+acts+'</div>';
546
421
  });
547
422
  el.innerHTML=h;el.scrollTop=el.scrollHeight;
548
423
  // Load fork info for messages that have IDs
@@ -574,7 +449,7 @@ function handleChatFile(input){
574
449
  chatAttachedFile={name:file.name,size:file.size,content:null,base64:base64,mimeType:'application/pdf',isPDF:true};
575
450
  chatAttachedImage=null;
576
451
  document.getElementById('chatAttachInfo').style.display='';
577
- document.getElementById('chatAttachName').textContent='📎 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
452
+ document.getElementById('chatAttachName').textContent='[file] '+file.name+' ('+Math.round(file.size/1024)+' KB)';
578
453
  };
579
454
  reader.readAsDataURL(file);
580
455
  }else{
@@ -583,7 +458,7 @@ function handleChatFile(input){
583
458
  chatAttachedFile={name:file.name,size:file.size,content:e.target.result};
584
459
  chatAttachedImage=null;
585
460
  document.getElementById('chatAttachInfo').style.display='';
586
- document.getElementById('chatAttachName').textContent='📎 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
461
+ document.getElementById('chatAttachName').textContent='[file] '+file.name+' ('+Math.round(file.size/1024)+' KB)';
587
462
  };
588
463
  reader.readAsText(file);
589
464
  }
@@ -597,7 +472,7 @@ function handleChatImage(input){
597
472
  chatAttachedImage={name:file.name,size:file.size,base64:base64,mimeType:file.type||'image/jpeg'};
598
473
  chatAttachedFile=null;
599
474
  document.getElementById('chatAttachInfo').style.display='';
600
- document.getElementById('chatAttachName').textContent='📷 '+file.name+' ('+Math.round(file.size/1024)+' KB)';
475
+ document.getElementById('chatAttachName').textContent='[img] '+file.name+' ('+Math.round(file.size/1024)+' KB)';
601
476
  };
602
477
  reader.readAsDataURL(file);
603
478
  }
@@ -693,7 +568,7 @@ function renderCanvasPanel(){
693
568
  else if(!item){t.textContent='Empty canvas';}
694
569
  else{t.textContent=(item.title||'Canvas')+(d.canvases.length>1?' ('+(canvasIdx+1)+'/'+d.canvases.length+')':'');}
695
570
  }
696
- // Nav arrowsonly for canvas mode (browser uses gallery grid)
571
+ // Nav arrows - only for canvas mode (browser uses gallery grid)
697
572
  var navEl=document.getElementById('canvasNav');
698
573
  if(navEl){navEl.style.display=d.canvases.length>1&&canvasMode==='canvas'?'flex':'none';}
699
574
  // Tab highlight
@@ -753,7 +628,7 @@ function canvasShowBrowser(){var d=getConvCanvasData();browserIdx=d.browsers.len
753
628
  function canvasShowCanvas(){var d=getConvCanvasData();canvasIdx=d.canvases.length-1;canvasMode='canvas';renderCanvasPanel();}
754
629
 
755
630
  function onConversationSwitch(){
756
- // Called when user switches conversationupdate canvas panel
631
+ // Called when user switches conversation - update canvas panel
757
632
  var p=document.getElementById('canvasPanel');
758
633
  if(p&&p.classList.contains('open')){
759
634
  var d=getConvCanvasData();
@@ -852,7 +727,7 @@ function editMsg(i){
852
727
  // User can press Esc to cancel or Enter to send (which creates a branch)
853
728
  inp.value=chatHistory[i].content;
854
729
  inp.focus();
855
- // Mark that we're editingsendChat will handle truncation
730
+ // Mark that we're editing - sendChat will handle truncation
856
731
  window._nhaEditIndex=i;
857
732
  // Listen for Esc to cancel
858
733
  inp.onkeydown=function(e){
@@ -898,7 +773,7 @@ function sendChat(){
898
773
  var isRetry=!!window._nhaRetryMode;
899
774
  window._nhaRetryMode=false;
900
775
 
901
- // Handle edit modetruncate history to edit point before adding
776
+ // Handle edit mode - truncate history to edit point before adding
902
777
  if(window._nhaEditIndex!=null){
903
778
  chatHistory=chatHistory.slice(0,window._nhaEditIndex);
904
779
  window._nhaEditIndex=null;
@@ -925,11 +800,6 @@ function sendChat(){
925
800
  clearChatAttach();
926
801
  apiPost('/api/chat',payload).then(function(r){
927
802
  chatHistory.pop();
928
- // Save llmContent in the user message so file context persists across turns
929
- if(r&&r.llmContent){
930
- var lastUser=chatHistory[chatHistory.length-1];
931
- if(lastUser&&lastUser.role==='user')lastUser.llmContent=r.llmContent;
932
- }
933
803
  if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
934
804
  else if(r&&r.error){chatHistory.push({role:'assistant',content:'Error: '+r.error})}
935
805
  else{chatHistory.push({role:'assistant',content:'Error: no response'})}
@@ -948,12 +818,12 @@ function sendChat(){
948
818
  chatHistory.push({role:'assistant',content:''});
949
819
  renderMessages();
950
820
  var streamIdx=chatHistory.length-1;
951
- var allHistory=chatHistory.slice(0,-1).map(function(m){var h={role:m.role,content:(m.content||'').replace(/!\\[Screenshot\\]\\(data:image\\/[^)]+\\)/g,'[Screenshot taken]')};if(m.llmContent)h.llmContent=m.llmContent;return h;});
821
+ var allHistory=chatHistory.slice(0,-1).map(function(m){return{role:m.role,content:(m.content||'').replace(/!\\[Screenshot\\]\\(data:image\\/[^)]+\\)/g,'[Screenshot taken]')};});
952
822
  var payload={message:msg,history:allHistory,conversationId:activeConvId,isRetry:isRetry};
953
823
 
954
824
  fetch(API+'/api/chat/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),signal:chatAbortController.signal}).then(function(response){
955
825
  if(!response.ok||!response.body||typeof response.body.getReader!=='function'){
956
- // Fallback for browsers without ReadableStream supportuse non-streaming endpoint
826
+ // Fallback for browsers without ReadableStream support - use non-streaming endpoint
957
827
  chatHistory[streamIdx].content='Thinking...';renderMessages();
958
828
  apiPost('/api/chat',{message:msg,history:allHistory,conversationId:activeConvId,isRetry:isRetry}).then(function(r){
959
829
  chatHistory[streamIdx].content=(r&&r.response)||(r&&r.error?'Error: '+r.error:'Error: no response');
@@ -984,7 +854,7 @@ function sendChat(){
984
854
  displayContent=displayContent.replace(/<think>[\\s\\S]*?<\\/think>/g,'').trim();
985
855
  }
986
856
  var el=document.getElementById('chatMessages');
987
- 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=displayContent||'Thinking...';}el.scrollTop=el.scrollHeight;}
857
+ if(el){var msgs=el.querySelectorAll('.msg');var last=msgs[msgs.length-1];if(last){var bub=last.querySelector('.msg__bubble');if(bub){bub.className='msg__bubble md-body';bub.innerHTML=isThinking?displayContent:renderMd(displayContent)||'Thinking...';}}el.scrollTop=el.scrollHeight;}
988
858
  }
989
859
  if(currentEvent==='tool'){
990
860
  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'};
@@ -1013,7 +883,7 @@ function sendChat(){
1013
883
  if(data.url)updateBrowserStatus(data.url);
1014
884
  }
1015
885
  if(currentEvent==='canvas'&&data.markers){
1016
- // Canvas content arrivedrender it immediately
886
+ // Canvas content arrived - render it immediately
1017
887
  var cm=data.markers.match(/\\[CANVAS_RENDER\\]([\\s\\S]*?)\\[\\/CANVAS_RENDER\\]/);
1018
888
  if(cm){try{var cd=JSON.parse(cm[1]);showCanvas(cd.html,cd.title);}catch(e){}}
1019
889
  if(data.markers.indexOf('[CANVAS_CLEAR]')!==-1)closeCanvas();
@@ -1055,7 +925,7 @@ function addTaskUI(){
1055
925
  });
1056
926
  }
1057
927
  function toggleTask(id){
1058
- // Optimistic UIupdate instantly, sync in background
928
+ // Optimistic UI - update instantly, sync in background
1059
929
  var t=dash.tasks.find(function(x){return x.id===id});
1060
930
  if(t){t.status=t.status==='done'?'pending':'done';t.completedAt=t.status==='done'?new Date().toISOString():null}
1061
931
  render();
@@ -1100,7 +970,7 @@ function refreshPlan(){
1100
970
  function renderEmails(el){
1101
971
  if(!dashLoaded.emails){el.innerHTML=loadingHTML('emails');return}
1102
972
  var e=dash.emails;
1103
- if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">Inbox zerono emails</div>';return}
973
+ if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">Inbox zero - no emails</div>';return}
1104
974
  var unreadCount=e.filter(function(x){return x.isUnread}).length;
1105
975
  var h='<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">';
1106
976
  h+='<span style="font-size:12px;color:var(--dim)">'+e.length+' emails'+(unreadCount>0?' ('+unreadCount+' unread)':'')+'</span>';
@@ -1222,7 +1092,7 @@ function renderCalendar(el){
1222
1092
  });
1223
1093
  h+='</div>';
1224
1094
 
1225
- // Calendar gridsquare cells
1095
+ // Calendar grid - square cells
1226
1096
  h+='<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px">';
1227
1097
  // Empty cells before first day
1228
1098
  for(var i=0;i<startDay;i++){h+='<div style="aspect-ratio:1;background:var(--bg);border-radius:6px"></div>'}
@@ -1319,16 +1189,18 @@ function openDayDetail(dateStr){
1319
1189
  });
1320
1190
  }
1321
1191
 
1322
- // Use the agent modal for day detail
1192
+ // Use the agent modal for day detail (read-only mode)
1193
+ selectedAgent=null;
1194
+ agentChatHistory=[];
1323
1195
  document.getElementById('modalName').textContent=dayLabel;
1324
- document.getElementById('modalPrompt').style.display='none';
1325
- document.getElementById('fileDropZone').style.display='none';
1326
- document.getElementById('fileInfo').style.display='none';
1327
- document.getElementById('modalResponse').style.display='block';
1328
- document.getElementById('modalResponse').innerHTML=h;
1196
+ document.getElementById('modalAgentDesc').textContent='';
1197
+ // Show the day events in the messages area
1198
+ var msgEl=document.getElementById('agentMessages');
1199
+ if(msgEl){msgEl.innerHTML='<div class="agent-chat__bubble agent-chat__bubble--agent md-body" style="width:100%;max-width:100%;box-sizing:border-box">'+h+'</div>';}
1200
+ // Hide input footer in read-only mode
1201
+ var footer=document.querySelector('.agent-chat__footer');
1202
+ if(footer)footer.style.display='none';
1329
1203
  document.getElementById('agentModal').classList.add('modal-overlay--open');
1330
- var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
1331
- if(sendBtn)sendBtn.style.display='none';
1332
1204
  }
1333
1205
 
1334
1206
  // ---- GITHUB ----
@@ -1422,7 +1294,7 @@ function renderBirthdays(el){
1422
1294
  bdays.forEach(function(b){
1423
1295
  var isToday=b.daysUntil===0;
1424
1296
  var label=isToday?'<span style="color:var(--red);font-weight:700">TODAY!</span>':b.daysUntil===1?'<span style="color:var(--amber)">Tomorrow</span>':'<span style="color:var(--dim)">in '+b.daysUntil+' days</span>';
1425
- h+='<div class="card" style="padding:12px 14px'+(isToday?';border-color:var(--red)':'')+'"><span style="font-size:16px">&#127874;</span> <span style="font-weight:700">'+esc(b.name)+'</span>'+esc(b.date)+' '+label+'</div>';
1297
+ h+='<div class="card" style="padding:12px 14px'+(isToday?';border-color:var(--red)':'')+'"><span style="font-size:16px">&#127874;</span> <span style="font-weight:700">'+esc(b.name)+'</span> - '+esc(b.date)+' '+label+'</div>';
1426
1298
  });
1427
1299
  el.innerHTML=h;
1428
1300
  });
@@ -1953,7 +1825,7 @@ var collabLastMessageCount=0;
1953
1825
  var collabUnreadCount=0;
1954
1826
  var collabGlobalPolling=null;
1955
1827
 
1956
- // No client-side polling neededserver pushes via WebSocket
1828
+ // No client-side polling needed - server pushes via WebSocket
1957
1829
  function startCollabGlobalPolling(){
1958
1830
  // Just load channels list once
1959
1831
  apiGet('/api/collab/channels').then(function(r){
@@ -1994,7 +1866,7 @@ setTimeout(startCollabGlobalPolling,2000);
1994
1866
 
1995
1867
  function renderCollab(el){
1996
1868
  var h='<div style="max-width:800px;margin:0 auto;padding:20px">';
1997
- h+='<h2 style="font-family:var(--term);color:var(--amber);font-size:18px;margin-bottom:16px">AgentMessengerEncrypted Communication</h2>';
1869
+ h+='<h2 style="font-family:var(--term);color:var(--amber);font-size:18px;margin-bottom:16px">AgentMessenger - Encrypted Communication</h2>';
1998
1870
 
1999
1871
  // Channel list
2000
1872
  h+='<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">';
@@ -2015,17 +1887,17 @@ function renderCollab(el){
2015
1887
  if(!collabActiveChannel){
2016
1888
  h+='<div style="padding:20px">';
2017
1889
  h+='<div style="text-align:center;margin-bottom:20px">';
2018
- h+='<div style="font-size:32px;margin-bottom:8px">🔐</div>';
1890
+ h+='<div style="font-size:32px;margin-bottom:8px">&#128272;</div>';
2019
1891
  h+='<div style="font-family:var(--term);color:var(--amber);font-size:16px;margin-bottom:4px">Alexandria</div>';
2020
1892
  h+='<div style="color:var(--dim);font-size:11px">E2E encrypted messaging for AI agents and teams</div>';
2021
1893
  h+='</div>';
2022
1894
  h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:10px">';
2023
1895
  h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">HOW TO USE</div>';
2024
- h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>1. Create a channel</b>Click [+ Create Channel] above. Give it a name.</div>';
1896
+ h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>1. Create a channel</b> - Click [+ Create Channel] above. Give it a name.</div>';
2025
1897
  h+='<div style="color:var(--dim);font-size:11px;margin-left:4px;margin-bottom:6px">You get an invite code. Share it with your team or another AI session.</div>';
2026
- h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>2. Others join</b>They click [Join Channel] and paste the invite code.</div>';
1898
+ h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>2. Others join</b> - They click [Join Channel] and paste the invite code.</div>';
2027
1899
  h+='<div style="color:var(--dim);font-size:11px;margin-left:4px;margin-bottom:6px">Works from this web UI, the Android app, or the CLI.</div>';
2028
- h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>3. Chat encrypted</b>All messages are E2E encrypted. The server sees only ciphertext.</div>';
1900
+ h+='<div style="color:var(--fg);font-size:12px;font-family:var(--mono);margin-bottom:4px"><b>3. Chat encrypted</b> - All messages are E2E encrypted. The server sees only ciphertext.</div>';
2029
1901
  h+='</div>';
2030
1902
  h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:10px">';
2031
1903
  h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">FROM CLI (same channels)</div>';
@@ -2037,10 +1909,10 @@ function renderCollab(el){
2037
1909
  h+='<div style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px">';
2038
1910
  h+='<div style="color:var(--amber);font-size:10px;font-family:var(--term);letter-spacing:1px;margin-bottom:8px">USE CASES</div>';
2039
1911
  h+='<div style="color:var(--dim);font-size:11px;font-family:var(--mono);line-height:18px">';
2040
- h+=' Two Claude Code instances sharing context in real-time<br>';
2041
- h+=' Team sharing AI analysis privately (security audits, code reviews)<br>';
2042
- h+=' Coordinating deployments between AI agents<br>';
2043
- h+=' Security briefings with auto-delete TTL</div>';
1912
+ h+='&#8226; Two Claude Code instances sharing context in real-time<br>';
1913
+ h+='&#8226; Team sharing AI analysis privately (security audits, code reviews)<br>';
1914
+ h+='&#8226; Coordinating deployments between AI agents<br>';
1915
+ h+='&#8226; Security briefings with auto-delete TTL</div>';
2044
1916
  h+='</div>';
2045
1917
  h+='</div>';
2046
1918
  }
@@ -2060,7 +1932,7 @@ function renderCollab(el){
2060
1932
 
2061
1933
  function renderCollabChannelList(){
2062
1934
  var el=document.getElementById('collabChannelList');if(!el)return;
2063
- if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yetclick [+ Create Channel] to start, or [Join Channel] to enter an invite code.</div>';return;}
1935
+ if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yet - click [+ Create Channel] to start, or [Join Channel] to enter an invite code.</div>';return;}
2064
1936
  var h='';
2065
1937
  for(var i=0;i<collabChannels.length;i++){
2066
1938
  var ch=collabChannels[i];
@@ -2113,7 +1985,7 @@ function collabDeleteChannel(id){
2113
1985
  function collabSelect(id){
2114
1986
  collabActiveChannel=id;
2115
1987
  collabLoadMessages();
2116
- // No pollingmessages arrive via WebSocket in real-time
1988
+ // No polling - messages arrive via WebSocket in real-time
2117
1989
  if(collabPolling)clearInterval(collabPolling);
2118
1990
  collabPolling=null;
2119
1991
  }
@@ -2147,7 +2019,7 @@ function collabSend(){
2147
2019
  inp.value='';
2148
2020
  apiPost('/api/collab/send',{channelId:collabActiveChannel,message:msg}).then(function(r){
2149
2021
  if(r.error){alert(r.error);return;}
2150
- // Message will arrive via WebSocketno need to reload
2022
+ // Message will arrive via WebSocket - no need to reload
2151
2023
  });
2152
2024
  }
2153
2025
 
@@ -2201,58 +2073,150 @@ function renderAgents(el){
2201
2073
  }
2202
2074
  var agentFilter=null;
2203
2075
 
2076
+ function openAgentEditor(mode, prefill) {
2077
+ // mode: 'create' | 'edit'
2078
+ var isEdit = mode === 'edit';
2079
+ var overlay = document.getElementById('agentEditorOverlay');
2080
+ overlay.innerHTML =
2081
+ '<div class="modal" style="max-width:560px">' +
2082
+ '<div class="modal__header">' +
2083
+ '<h2 style="color:var(--green)">'+(isEdit?'\u270F\uFE0F Edit Agent':'\u2795 New Agent')+'</h2>' +
2084
+ '<button class="modal__close" onclick="closeAgentEditor()">&times;</button>' +
2085
+ '</div>' +
2086
+ '<div class="modal__body" style="display:flex;flex-direction:column;gap:12px">' +
2087
+ (isEdit ? '' :
2088
+ '<div><label style="font-size:11px;color:var(--dim);display:block;margin-bottom:4px">Agent name (lowercase, no spaces)</label>' +
2089
+ '<input id="aeNameField" placeholder="my-agent" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:13px"></div>'
2090
+ ) +
2091
+ '<div><label style="font-size:11px;color:var(--dim);display:block;margin-bottom:4px">Tagline</label>' +
2092
+ '<input id="aeTaglineField" value="'+(prefill&&prefill.tagline?esc(prefill.tagline):'')+'" placeholder="Short description of what this agent does" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:13px"></div>' +
2093
+ '<div><label style="font-size:11px;color:var(--dim);display:block;margin-bottom:4px">System Prompt</label>' +
2094
+ '<textarea id="aeSysPromptField" placeholder="You are an expert in... Your job is to..." style="width:100%;min-height:160px;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--text);font-size:12px;font-family:var(--mono);resize:vertical">'+(prefill&&prefill.systemPrompt?esc(prefill.systemPrompt):'')+'</textarea></div>' +
2095
+ '<div id="aeError" style="color:var(--red);font-size:11px;display:none"></div>' +
2096
+ '</div>' +
2097
+ '<div class="modal__footer">' +
2098
+ '<button class="btn btn--secondary" onclick="closeAgentEditor()">Cancel</button>' +
2099
+ '<button class="btn btn--primary" id="aeSubmitBtn" onclick="submitAgentEditor(\\''+mode+'\\',\\''+esc((prefill&&prefill.name)||'')+'\\')">'+(isEdit?'Save Changes':'Create Agent')+'</button>' +
2100
+ '</div>' +
2101
+ '</div>';
2102
+ overlay.style.display='flex';
2103
+ }
2104
+
2105
+ function closeAgentEditor(){
2106
+ var overlay=document.getElementById('agentEditorOverlay');
2107
+ if(overlay){overlay.style.display='none';overlay.innerHTML='';}
2108
+ }
2109
+
2110
+ function submitAgentEditor(mode, existingName){
2111
+ var tagline=(document.getElementById('aeTaglineField')||{}).value||'';
2112
+ var sysPrompt=(document.getElementById('aeSysPromptField')||{}).value||'';
2113
+ var errEl=document.getElementById('aeError');
2114
+ var btn=document.getElementById('aeSubmitBtn');
2115
+ if(!tagline.trim()||!sysPrompt.trim()){if(errEl){errEl.textContent='Tagline and system prompt are required.';errEl.style.display='';}return;}
2116
+
2117
+ if(mode==='create'){
2118
+ var nameEl=document.getElementById('aeNameField');
2119
+ var name=(nameEl?nameEl.value:'').toLowerCase().replace(/[^a-z0-9_-]/g,'');
2120
+ if(!name){if(errEl){errEl.textContent='Agent name is required (lowercase, no spaces).';errEl.style.display='';}return;}
2121
+ if(btn)btn.disabled=true;
2122
+ apiPost('/api/agents',{name:name,tagline:tagline,systemPrompt:sysPrompt}).then(function(r){
2123
+ if(r&&r.ok){closeAgentEditor();showToast('success','Agent Created',name.toUpperCase()+' is ready to use');loadAgents().then(function(){renderAgents(document.getElementById('content'));});}
2124
+ else{if(errEl){errEl.textContent='Error: '+(r&&r.error||'Unknown');errEl.style.display='';}if(btn)btn.disabled=false;}
2125
+ });
2126
+ } else {
2127
+ if(btn)btn.disabled=true;
2128
+ fetch(API+'/api/agents/'+existingName,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({tagline:tagline,systemPrompt:sysPrompt,category:'custom'})}).then(function(r){return r.json();}).then(function(r){
2129
+ if(r&&r.ok){closeAgentEditor();showToast('success','Agent Updated',existingName.toUpperCase()+' updated');loadAgents().then(function(){renderAgents(document.getElementById('content'));});}
2130
+ else{if(errEl){errEl.textContent='Error: '+(r&&r.error||'Unknown');errEl.style.display='';}if(btn)btn.disabled=false;}
2131
+ });
2132
+ }
2133
+ }
2134
+
2204
2135
  function showCreateAgentForm(){
2205
- var name=prompt('Agent name (lowercase, no spaces):');
2206
- if(!name)return;
2207
- name=name.toLowerCase().replace(/[^a-z0-9_-]/g,'');
2208
- if(!name)return;
2209
- var tagline=prompt('Tagline (short description):');
2210
- if(!tagline)return;
2211
- var sysPrompt=prompt('System prompt (agent personality & instructions):');
2212
- if(!sysPrompt)return;
2213
- apiPost('/api/agents',{name:name,tagline:tagline,systemPrompt:sysPrompt}).then(function(r){
2214
- if(r&&r.ok){showToast('success','Agent Created',name.toUpperCase()+' is ready to use');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
2215
- else{alert('Error: '+(r&&r.error||'Unknown'));}
2216
- });
2136
+ openAgentEditor('create', null);
2217
2137
  }
2218
2138
 
2219
2139
  function editAgent(name){
2220
- fetch('/api/agents/'+name).then(function(r){return r.json()}).then(function(data){
2221
- var newTagline=prompt('Tagline:',data.tagline||'');
2222
- if(newTagline===null)return;
2223
- var newPrompt=prompt('System prompt:',data.systemPrompt||'');
2224
- if(newPrompt===null)return;
2225
- 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){
2226
- if(r&&r.ok){showToast('success','Agent Updated',name.toUpperCase()+' updated');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
2227
- else{alert('Error: '+(r&&r.error||'Unknown'));}
2228
- });
2140
+ fetch(API+'/api/agents/'+name).then(function(r){return r.json();}).then(function(data){
2141
+ openAgentEditor('edit', {name:name, tagline:data.tagline||'', systemPrompt:data.systemPrompt||''});
2229
2142
  });
2230
2143
  }
2231
2144
 
2232
2145
  function deleteAgent(name){
2233
- if(!confirm('Delete agent "'+name+'"? This cannot be undone.'))return;
2234
- fetch('/api/agents/'+name,{method:'DELETE'}).then(function(r){return r.json()}).then(function(r){
2235
- if(r&&r.ok){showToast('success','Agent Deleted',name+' removed');loadAgents().then(function(){renderAgents(document.getElementById('content'))});}
2236
- else{alert('Error: '+(r&&r.error||'Unknown'));}
2146
+ // Inline confirm via toast-style overlay
2147
+ var overlay=document.getElementById('agentEditorOverlay');
2148
+ overlay.innerHTML=
2149
+ '<div class="modal" style="max-width:380px">' +
2150
+ '<div class="modal__header"><h2 style="color:var(--red)">\u26A0\uFE0F Delete Agent</h2><button class="modal__close" onclick="closeAgentEditor()">&times;</button></div>' +
2151
+ '<div class="modal__body"><p style="color:var(--bright);font-size:14px">Delete <strong>'+esc(name)+'</strong>?</p><p style="color:var(--dim);font-size:12px">This cannot be undone.</p></div>' +
2152
+ '<div class="modal__footer">' +
2153
+ '<button class="btn btn--secondary" onclick="closeAgentEditor()">Cancel</button>' +
2154
+ '<button class="btn btn--primary" style="background:var(--red)" onclick="confirmDeleteAgent(\\''+esc(name)+'\\')">Delete</button>' +
2155
+ '</div>' +
2156
+ '</div>';
2157
+ overlay.style.display='flex';
2158
+ }
2159
+
2160
+ function confirmDeleteAgent(name){
2161
+ closeAgentEditor();
2162
+ fetch(API+'/api/agents/'+name,{method:'DELETE'}).then(function(r){return r.json();}).then(function(r){
2163
+ if(r&&r.ok){showToast('success','Agent Deleted',name+' removed');loadAgents().then(function(){renderAgents(document.getElementById('content'));});}
2164
+ else{showToast('error','Delete Failed',r&&r.error||'Unknown error');}
2237
2165
  });
2238
2166
  }
2239
2167
 
2168
+ function renderAgentMessages(){
2169
+ var el=document.getElementById('agentMessages');
2170
+ if(!el)return;
2171
+ if(agentChatHistory.length===0){
2172
+ var icon=AGENT_ICONS[selectedAgent]||'\uD83E\uDD16';
2173
+ var desc=AGENT_DESCRIPTIONS[selectedAgent]||'';
2174
+ el.innerHTML='<div style="text-align:center;padding:24px 8px;color:var(--dim);font-size:12px">'+
2175
+ '<div style="font-size:28px;margin-bottom:6px">'+icon+'</div>'+
2176
+ '<div style="color:var(--fg);font-weight:600;margin-bottom:4px">'+esc(selectedAgent.toUpperCase())+'</div>'+
2177
+ (desc?'<div>'+esc(desc)+'</div>':'')+
2178
+ '</div>';
2179
+ return;
2180
+ }
2181
+ el.innerHTML=agentChatHistory.map(function(m){
2182
+ var cls='agent-chat__bubble agent-chat__bubble--'+(m.role==='user'?'user':'agent');
2183
+ var content;
2184
+ if(m.role==='user'){
2185
+ content=esc(m.text);
2186
+ } else if(m.waiting||(m.streaming&&!m.text)){
2187
+ content='<span class="thinking-dots"><span></span><span></span><span></span></span>';
2188
+ } else if(m.streaming){
2189
+ content=esc(m.text)+'\u258B';
2190
+ } else {
2191
+ content=renderMd(m.text);
2192
+ }
2193
+ return '<div class="'+cls+' '+(m.role==='agent'?'md-body':'')+'">'+content+'</div>';
2194
+ }).join('');
2195
+ el.scrollTop=el.scrollHeight;
2196
+ }
2197
+
2240
2198
  function openAgent(name,display){
2241
2199
  selectedAgent=name;
2200
+ agentChatHistory=[];
2242
2201
  attachedFileContent=null;attachedFileName=null;
2243
- document.getElementById('modalName').textContent=display||name;
2202
+ var icon=AGENT_ICONS[name.toLowerCase()]||'\u{1F916}';
2203
+ var desc=AGENT_DESCRIPTIONS[name.toLowerCase()]||'';
2204
+ var nameEl=document.getElementById('modalName');
2205
+ nameEl.innerHTML=icon+' <span style="color:var(--green)">'+esc(display||name)+'</span>';
2206
+ var subEl=document.getElementById('modalAgentDesc');
2207
+ if(subEl){subEl.textContent=desc;}
2244
2208
  document.getElementById('modalPrompt').value='';
2245
- document.getElementById('modalPrompt').style.display='';
2246
- document.getElementById('modalResponse').style.display='none';
2247
- document.getElementById('modalResponse').textContent='';
2248
- document.getElementById('modalResponse').innerHTML='';
2249
- document.getElementById('fileInfo').style.display='none';
2250
- document.getElementById('fileDropZone').style.display='';
2251
- document.getElementById('fileDropZone').style.borderColor='var(--border2)';
2252
- document.getElementById('fileInput').value='';
2253
- var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
2254
- if(sendBtn)sendBtn.style.display='';
2209
+ document.getElementById('agentFileInfo').style.display='none';
2210
+ document.getElementById('agentFileInput').value='';
2211
+ document.getElementById('agentFileDropZone').style.borderColor='';
2212
+ var askBtn=document.getElementById('agentAskBtn');
2213
+ if(askBtn){askBtn.disabled=false;askBtn.textContent='Send';}
2214
+ // Ensure footer is visible (may be hidden from calendar day-detail view)
2215
+ var footer=document.querySelector('.agent-chat__footer');
2216
+ if(footer)footer.style.display='';
2217
+ renderAgentMessages();
2255
2218
  document.getElementById('agentModal').classList.add('modal-overlay--open');
2219
+ setTimeout(function(){var i=document.getElementById('modalPrompt');if(i)i.focus();},100);
2256
2220
  }
2257
2221
  function closeModal(){
2258
2222
  document.getElementById('agentModal').classList.remove('modal-overlay--open');
@@ -2306,11 +2270,14 @@ function renderSettings(el) {
2306
2270
  ['profile-notes', 'Notes', 'Anything else agents should know about you'],
2307
2271
  ]) +
2308
2272
  '<div style="padding:12px 16px;margin-bottom:16px;background:var(--amberdim);border:1px solid var(--amber3);border-radius:8px"><span style="font-family:var(--term);color:var(--amber);font-size:13px;font-weight:700">NHA Free (Liara)</span><div style="font-size:11px;color:var(--dim);margin:4px 0 8px">Powered by Qwen3 32B. Free, no API key needed. Slower (5-15s).</div><button onclick="apiPost(\\x27/api/config\\x27,{key:\\x27provider\\x27,value:\\x27nha\\x27}).then(function(){location.reload()})" style="padding:6px 16px;background:var(--amber3);color:var(--bg);border:none;border-radius:6px;cursor:pointer;font-family:var(--mono);font-size:11px;font-weight:700">Use NHA Free</button></div>' +
2273
+ settingsSection('language', 'Language / Lingua', 'Set the UI language. Applies on reload.', [
2274
+ ['lang', 'Language', 'en / it / es / fr / de / pt / zh / ja / ar / hi / ru / nl / pl / tr / ko / sv / da / fi / no / cs'],
2275
+ ]) +
2309
2276
  settingsSection('llm', 'LLM Provider', 'Or use your own API key for faster, more capable responses.', [
2310
2277
  ['provider', 'Provider', 'nha (free) / anthropic / openai / gemini / deepseek / grok / mistral'],
2311
2278
  ['key', 'API Key', 'Not needed for NHA Free', true],
2312
2279
  ['model', 'Model', 'Leave empty for default'],
2313
- ['thinking', 'Extended Thinking', 'on / offQwen3 reasoning mode (NHA Free only)'],
2280
+ ['thinking', 'Extended Thinking', 'on / off - Qwen3 reasoning mode (NHA Free only)'],
2314
2281
  ]) +
2315
2282
  settingsSection('responder', 'Message Responder', 'Auto-reply to Telegram and Discord messages.', [
2316
2283
  ['telegram-bot-token', 'Telegram Bot Token', 'Get from @BotFather', true],
@@ -2321,7 +2288,6 @@ function renderSettings(el) {
2321
2288
  ['summary-time', 'Summary Time', '18:00'],
2322
2289
  ['meeting-alert', 'Meeting Alert (minutes)', '30'],
2323
2290
  ]) +
2324
- renderEmailAccountsSection() +
2325
2291
  '<div class="card" style="margin-top:16px"><div class="card__title">Google Account</div>' +
2326
2292
  '<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>' +
2327
2293
  (settingsData.hasGoogle ? '<div style="color:var(--green);font-size:12px;margin-bottom:8px">\\u2705 Connected</div>' : '') +
@@ -2342,92 +2308,6 @@ function connectGoogle() {
2342
2308
  });
2343
2309
  }
2344
2310
 
2345
- function renderEmailAccountsSection() {
2346
- var accounts = settingsData.emailAccounts || [];
2347
- var h = '<div class="card" style="margin-bottom:16px"><div class="card__title" style="color:var(--green);font-size:14px;margin-bottom:4px">Email Accounts (IMAP/SMTP)</div>';
2348
- h += '<div style="font-size:11px;color:var(--dim);margin-bottom:12px">Add email accounts for multi-provider inbox. Read-only IMAP (never deletes from server) + SMTP for sending. Works alongside Gmail.</div>';
2349
-
2350
- if (accounts.length > 0) {
2351
- for (var i = 0; i < accounts.length; i++) {
2352
- var a = accounts[i];
2353
- h += '<div style="padding:8px 12px;margin-bottom:8px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;display:flex;align-items:center;justify-content:space-between">';
2354
- h += '<div><span style="color:var(--green);font-weight:700;font-size:12px">' + esc(a.label || 'Email') + '</span>';
2355
- h += '<span style="color:var(--dim);font-size:11px;margin-left:8px">' + esc(a.address || '') + '</span>';
2356
- if (a.isDefault) h += '<span style="color:var(--amber);font-size:9px;margin-left:6px">(default)</span>';
2357
- h += '</div>';
2358
- h += '<div><button onclick="setDefaultEmail(' + i + ')" style="background:none;border:1px solid var(--border);color:var(--dim);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:10px;margin-right:4px" title="Set as default">Default</button>';
2359
- h += '<button onclick="removeEmailAccount(' + i + ')" style="background:none;border:1px solid var(--red3);color:var(--red);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:10px" title="Remove">Remove</button></div></div>';
2360
- }
2361
- } else {
2362
- h += '<div style="color:var(--dim);font-size:11px;padding:8px 0">No email accounts configured.</div>';
2363
- }
2364
-
2365
- h += '<div style="margin-top:12px;border-top:1px solid var(--border);padding-top:12px">';
2366
- h += '<div style="font-size:12px;color:var(--fg);font-weight:700;margin-bottom:8px">Add Email Account</div>';
2367
- var fields = [
2368
- ['ea_label', 'Label', 'e.g. Work, Personal'],
2369
- ['ea_address', 'Email Address', 'e.g. you@company.com'],
2370
- ['ea_imap_host', 'IMAP Host', 'e.g. imap.company.com'],
2371
- ['ea_imap_port', 'IMAP Port', '993'],
2372
- ['ea_imap_user', 'IMAP User', 'Usually your email address'],
2373
- ['ea_imap_pass', 'IMAP Password', 'App password recommended', true],
2374
- ['ea_smtp_host', 'SMTP Host', 'e.g. smtp.company.com'],
2375
- ['ea_smtp_port', 'SMTP Port', '587'],
2376
- ['ea_smtp_user', 'SMTP User', 'Usually same as IMAP'],
2377
- ['ea_smtp_pass', 'SMTP Password', 'Usually same as IMAP', true],
2378
- ];
2379
- for (var i = 0; i < fields.length; i++) {
2380
- var f = fields[i];
2381
- h += '<div style="margin-bottom:6px"><label style="display:block;font-size:10px;color:var(--dim);margin-bottom:2px">' + esc(f[1]) + '</label>';
2382
- h += '<input id="' + f[0] + '" type="' + (f[3] ? 'password' : 'text') + '" placeholder="' + esc(f[2]) + '" style="width:100%;padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px;font-family:var(--mono)"></div>';
2383
- }
2384
- h += '<button onclick="addEmailAccount()" style="margin-top:8px;padding:8px 20px;background:var(--green3);color:var(--bg);border:none;border-radius:6px;cursor:pointer;font-weight:700;font-size:12px">Add Account</button>';
2385
- h += '<div id="emailAccountStatus" style="margin-top:6px;font-size:10px;color:var(--dim)"></div>';
2386
- h += '</div></div>';
2387
- return h;
2388
- }
2389
-
2390
- function addEmailAccount() {
2391
- var label = document.getElementById('ea_label').value.trim();
2392
- var address = document.getElementById('ea_address').value.trim();
2393
- var imapHost = document.getElementById('ea_imap_host').value.trim();
2394
- var imapPort = document.getElementById('ea_imap_port').value.trim() || '993';
2395
- var imapUser = document.getElementById('ea_imap_user').value.trim() || address;
2396
- var imapPass = document.getElementById('ea_imap_pass').value;
2397
- var smtpHost = document.getElementById('ea_smtp_host').value.trim();
2398
- var smtpPort = document.getElementById('ea_smtp_port').value.trim() || '587';
2399
- var smtpUser = document.getElementById('ea_smtp_user').value.trim() || imapUser;
2400
- var smtpPass = document.getElementById('ea_smtp_pass').value || imapPass;
2401
-
2402
- if (!address || !imapHost) {
2403
- var s = document.getElementById('emailAccountStatus');
2404
- if (s) { s.textContent = 'Email address and IMAP host are required.'; s.style.color = 'var(--red)'; }
2405
- return;
2406
- }
2407
-
2408
- var val = [label, address, imapHost, imapPort, imapUser, imapPass, smtpHost, smtpPort, smtpUser, smtpPass].join('|');
2409
- apiPost('/api/config', { key: 'email-add', value: val }).then(function() {
2410
- var s = document.getElementById('emailAccountStatus');
2411
- if (s) { s.textContent = 'Account added!'; s.style.color = 'var(--green)'; }
2412
- settingsLoaded = false;
2413
- renderSettings(document.getElementById('mainContent'));
2414
- });
2415
- }
2416
-
2417
- function removeEmailAccount(idx) {
2418
- apiPost('/api/config', { key: 'email-remove', value: String(idx) }).then(function() {
2419
- settingsLoaded = false;
2420
- renderSettings(document.getElementById('mainContent'));
2421
- });
2422
- }
2423
-
2424
- function setDefaultEmail(idx) {
2425
- apiPost('/api/config', { key: 'email-default', value: String(idx) }).then(function() {
2426
- settingsLoaded = false;
2427
- renderSettings(document.getElementById('mainContent'));
2428
- });
2429
- }
2430
-
2431
2311
  function settingsSection(id, title, desc, fields) {
2432
2312
  var h = '<form class="card" style="margin-bottom:16px" id="settings-' + id + '" onsubmit="event.preventDefault();saveSettingsSection(\\x27' + id + '\\x27)">' +
2433
2313
  '<div class="card__title" style="color:var(--green);font-size:14px;margin-bottom:4px">' + esc(title) + '</div>' +
@@ -2445,7 +2325,7 @@ function settingsSection(id, title, desc, fields) {
2445
2325
  if (key === 'provider') {
2446
2326
  // Dropdown for provider selection
2447
2327
  var providers = [
2448
- {value:'nha',label:'NHA Free (Liara)no API key needed'},
2328
+ {value:'nha',label:'NHA Free (Liara) - no API key needed'},
2449
2329
  {value:'anthropic',label:'Anthropic (Claude)'},
2450
2330
  {value:'openai',label:'OpenAI (GPT-4)'},
2451
2331
  {value:'gemini',label:'Google (Gemini)'},
@@ -2463,8 +2343,8 @@ function settingsSection(id, title, desc, fields) {
2463
2343
  } else if (key === 'thinking') {
2464
2344
  // Dropdown for thinking toggle
2465
2345
  h += '<select style="width:100%;padding:8px 12px;font-size:13px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:var(--r)" data-config-key="thinking" data-section="' + esc(id) + '">' +
2466
- '<option value="off"' + (currentVal !== 'on' ? ' selected' : '') + '>Offfaster responses</option>' +
2467
- '<option value="on"' + (currentVal === 'on' ? ' selected' : '') + '>Onextended reasoning (NHA Free only)</option>' +
2346
+ '<option value="off"' + (currentVal !== 'on' ? ' selected' : '') + '>Off - faster responses</option>' +
2347
+ '<option value="on"' + (currentVal === 'on' ? ' selected' : '') + '>On - extended reasoning (NHA Free only)</option>' +
2468
2348
  '</select>';
2469
2349
  } else {
2470
2350
  h += '<input type="' + (isSecret ? 'password' : 'text') + '" ' +
@@ -2542,46 +2422,102 @@ function handleFileSelect(input) {
2542
2422
  if (file) readFile(file);
2543
2423
  }
2544
2424
  function readFile(file) {
2425
+ var infoId = document.getElementById('agentFileInfo') ? 'agentFileInfo' : 'fileInfo';
2426
+ var dropId = document.getElementById('agentFileDropZone') ? 'agentFileDropZone' : 'fileDropZone';
2427
+ var infoEl = document.getElementById(infoId);
2428
+ var dropEl = document.getElementById(dropId);
2545
2429
  if (file.size > 500000) {
2546
- document.getElementById('fileInfo').style.display = 'block';
2547
- document.getElementById('fileInfo').textContent = 'File too large (max 500KB)';
2548
- document.getElementById('fileInfo').style.color = 'var(--red)';
2430
+ if(infoEl){infoEl.style.display='block';infoEl.style.color='var(--red)';infoEl.textContent='File too large (max 500KB)';}
2549
2431
  return;
2550
2432
  }
2551
2433
  var reader = new FileReader();
2552
2434
  reader.onload = function(e) {
2553
2435
  attachedFileContent = e.target.result;
2554
2436
  attachedFileName = file.name;
2555
- var info = document.getElementById('fileInfo');
2556
- info.style.display = 'block';
2557
- info.style.color = 'var(--cyan)';
2558
- info.textContent = 'Attached: ' + file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
2559
- document.getElementById('fileDropZone').style.borderColor = 'var(--green)';
2437
+ if(infoEl){infoEl.style.display='block';infoEl.style.color='var(--cyan)';infoEl.textContent='Attached: '+file.name+' ('+(file.size/1024).toFixed(1)+' KB)';}
2438
+ if(dropEl){dropEl.style.borderColor='var(--green)';}
2560
2439
  };
2561
2440
  reader.readAsText(file);
2562
2441
  }
2563
2442
 
2443
+ var agentAbortController = null;
2444
+
2564
2445
  function askAgent(){
2565
2446
  var p=document.getElementById('modalPrompt').value.trim();if(!p||!selectedAgent)return;
2566
- var resp=document.getElementById('modalResponse');
2567
- resp.style.display='block';resp.textContent='Thinking...';
2568
2447
 
2569
- var payload = {agent:selectedAgent, prompt:p};
2570
- if (attachedFileContent) {
2571
- payload.fileContent = attachedFileContent;
2572
- payload.fileName = attachedFileName;
2448
+ // Add user message to history
2449
+ agentChatHistory.push({role:'user',text:p});
2450
+ // Add waiting placeholder — shows dots until first token arrives
2451
+ agentChatHistory.push({role:'agent',text:'',waiting:true});
2452
+ renderAgentMessages();
2453
+ document.getElementById('modalPrompt').value='';
2454
+
2455
+ // Abort any previous stream
2456
+ if(agentAbortController){try{agentAbortController.abort();}catch(e){}}
2457
+ agentAbortController=new AbortController();
2458
+
2459
+ var payload={agent:selectedAgent,prompt:p};
2460
+ if(attachedFileContent){payload.fileContent=attachedFileContent;payload.fileName=attachedFileName;}
2461
+
2462
+ // Disable Send button while streaming
2463
+ var askBtn=document.getElementById('agentAskBtn');
2464
+ if(askBtn){askBtn.disabled=true;askBtn.textContent='...';}
2465
+
2466
+ var agentMsgIdx=agentChatHistory.length-1; // index of the streaming placeholder
2467
+
2468
+ function updateStreamingBubble(text,done){
2469
+ agentChatHistory[agentMsgIdx]={role:'agent',text:text,waiting:false,streaming:!done};
2470
+ renderAgentMessages();
2573
2471
  }
2574
2472
 
2575
- apiPost('/api/ask', payload).then(function(r){
2576
- if(r&&r.error) resp.textContent='Error: '+r.error;
2577
- else if(r&&r.response) resp.textContent=r.response;
2578
- else resp.textContent='Error: no response. Run "nha update" in Termux to download agents.';
2579
- // Reset file after ask
2580
- attachedFileContent = null;
2581
- attachedFileName = null;
2582
- document.getElementById('fileInfo').style.display = 'none';
2583
- document.getElementById('fileDropZone').style.borderColor = 'var(--border2)';
2584
- document.getElementById('fileInput').value = '';
2473
+ fetch(API+'/api/ask/stream',{
2474
+ method:'POST',
2475
+ headers:{'Content-Type':'application/json'},
2476
+ body:JSON.stringify(payload),
2477
+ signal:agentAbortController.signal
2478
+ }).then(function(response){
2479
+ var reader=response.body.getReader();
2480
+ var decoder=new TextDecoder();
2481
+ var buf='';
2482
+ var accumulated='';
2483
+ function pump(){
2484
+ return reader.read().then(function(result){
2485
+ if(result.done){
2486
+ updateStreamingBubble(accumulated||'(empty response)',true);
2487
+ if(askBtn){askBtn.disabled=false;askBtn.textContent='Send';}
2488
+ return;
2489
+ }
2490
+ buf+=decoder.decode(result.value,{stream:true});
2491
+ var lines=buf.split(String.fromCharCode(10));
2492
+ buf=lines.pop();
2493
+ lines.forEach(function(line){
2494
+ if(line.indexOf('data: ')!==0)return;
2495
+ try{
2496
+ var ev=JSON.parse(line.slice(6));
2497
+ if(ev.token){accumulated+=ev.token;updateStreamingBubble(accumulated,false);}
2498
+ if(ev.done){
2499
+ updateStreamingBubble(accumulated||'(empty response)',true);
2500
+ if(askBtn){askBtn.disabled=false;askBtn.textContent='Send';}
2501
+ attachedFileContent=null;attachedFileName=null;
2502
+ document.getElementById('agentFileInfo').style.display='none';
2503
+ document.getElementById('agentFileInput').value='';
2504
+ document.getElementById('agentFileDropZone').style.borderColor='';
2505
+ }
2506
+ if(ev.error){
2507
+ updateStreamingBubble('\u26a0\ufe0f Error: '+ev.error,true);
2508
+ if(askBtn){askBtn.disabled=false;askBtn.textContent='Send';}
2509
+ }
2510
+ }catch(e){}
2511
+ });
2512
+ return pump();
2513
+ });
2514
+ }
2515
+ return pump();
2516
+ }).catch(function(err){
2517
+ if(err.name!=='AbortError'){
2518
+ updateStreamingBubble('\u26a0\ufe0f Stream error: '+err.message,true);
2519
+ }
2520
+ if(askBtn){askBtn.disabled=false;askBtn.textContent='Send';}
2585
2521
  });
2586
2522
  }
2587
2523
 
@@ -2671,7 +2607,7 @@ function handleDaemonEvent(msg) {
2671
2607
  break;
2672
2608
 
2673
2609
  case 'security_alert':
2674
- showToast('security', 'Security Alert', 'Suspicious: ' + (msg.data.from || '') + '' + (msg.data.subject || ''), 20000);
2610
+ showToast('security', 'Security Alert', 'Suspicious: ' + (msg.data.from || '') + ' - ' + (msg.data.subject || ''), 20000);
2675
2611
  break;
2676
2612
 
2677
2613
  case 'plan_ready':
@@ -2692,7 +2628,7 @@ function handleDaemonEvent(msg) {
2692
2628
  renderCollabMessages();
2693
2629
  }
2694
2630
  } else {
2695
- // Not viewing this channelshow badge + toast
2631
+ // Not viewing this channel - show badge + toast
2696
2632
  collabUnreadCount++;
2697
2633
  updateCollabBadge();
2698
2634
  showToast('collab', 'AgentMessenger', cm.senderName + ': ' + (cm.content || '').slice(0, 100), 5000);
@@ -2769,6 +2705,667 @@ function stopVoiceInput() {
2769
2705
  }
2770
2706
  }
2771
2707
 
2708
+ // ---- STUDIO ----
2709
+ var studioState = {
2710
+ task: '',
2711
+ nodes: [], // [{icon,agent,label,status:'waiting'|'running'|'done'|'error'}]
2712
+ log: [], // [{agent,icon,text,time,type:'agent'|'system'|'error'}]
2713
+ result: '',
2714
+ running: false,
2715
+ planned: false
2716
+ };
2717
+
2718
+ var STUDIO_EXAMPLES = [
2719
+ 'Analyze my unread emails and create a priority action plan',
2720
+ 'Search the web for AI news today and summarize it in a canvas report',
2721
+ 'Check my calendar for this week and suggest how to optimize my schedule',
2722
+ 'Review my GitHub notifications and draft responses to open issues',
2723
+ 'Search for information about a topic, fact-check it, and write a report'
2724
+ ];
2725
+
2726
+ function studioLog(agent, icon, text, type, update) {
2727
+ // update=true: update the last log entry for this agent instead of adding a new one
2728
+ var time = new Date().toLocaleTimeString('en', {hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false});
2729
+ if (update && studioState.log.length) {
2730
+ var last = studioState.log[studioState.log.length - 1];
2731
+ if (last.agent === agent) { last.text = text; last.type = type || last.type; renderStudioLog(); return; }
2732
+ }
2733
+ studioState.log.push({agent: agent, icon: icon, text: text, time: time, type: type||'agent'});
2734
+ renderStudioLog();
2735
+ }
2736
+
2737
+ function renderStudioNodes() {
2738
+ var el = document.getElementById('studioNodes');
2739
+ if (!el) return;
2740
+ if (!studioState.nodes.length) {
2741
+ el.innerHTML = '<div class="studio-canvas__empty"><div class="studio-canvas__empty-icon">&#9881;</div><div>Describe a task and click Run</div></div>';
2742
+ return;
2743
+ }
2744
+ var html = '<div class="studio-nodes">';
2745
+ studioState.nodes.forEach(function(n, i) {
2746
+ var cls = 'studio-node';
2747
+ if (n.status === 'running') cls += ' studio-node--active';
2748
+ else if (n.status === 'done') cls += ' studio-node--done';
2749
+ else if (n.status === 'error') cls += ' studio-node--error';
2750
+ var statusLabel = {waiting:'&#9711; wait', running:'&#9654; running', done:'&#10003; done', error:'&#10005; error'}[n.status] || '';
2751
+ html += '<div class="' + cls + '">';
2752
+ html += '<div class="studio-node__circle">' + n.icon + '</div>';
2753
+ html += '<div class="studio-node__label">' + esc(n.label) + '</div>';
2754
+ html += '<div class="studio-node__status studio-node__status--' + n.status + '">' + statusLabel + '</div>';
2755
+ html += '</div>';
2756
+ if (i < studioState.nodes.length - 1) {
2757
+ var next = studioState.nodes[i + 1];
2758
+ var arrowCls = 'studio-arrow';
2759
+ if (n.status === 'done' && next.status === 'running') arrowCls += ' studio-arrow--active';
2760
+ else if (n.status === 'done') arrowCls += ' studio-arrow--done';
2761
+ html += '<div class="' + arrowCls + '">&#8594;</div>';
2762
+ }
2763
+ });
2764
+ html += '</div>';
2765
+ el.innerHTML = html;
2766
+ }
2767
+
2768
+ function renderStudioLog() {
2769
+ var el = document.getElementById('studioLog');
2770
+ if (!el) return;
2771
+ if (!studioState.log.length) { el.style.display = 'none'; return; }
2772
+ el.style.display = 'block';
2773
+ el.innerHTML = studioState.log.map(function(e) {
2774
+ var cls = 'studio-log-entry' + (e.type === 'system' ? ' studio-log-entry--system' : e.type === 'error' ? ' studio-log-entry--error' : '');
2775
+ return '<div class="' + cls + '">' +
2776
+ '<div class="studio-log-entry__header">' +
2777
+ '<span class="studio-log-entry__icon">' + e.icon + '</span>' +
2778
+ '<span class="studio-log-entry__agent">' + esc(e.agent) + '</span>' +
2779
+ '<span class="studio-log-entry__time">' + esc(e.time) + '</span>' +
2780
+ '</div>' +
2781
+ '<div class="studio-log-entry__text md-body">' + renderMd(e.text) + '</div>' +
2782
+ '</div>';
2783
+ }).join('');
2784
+ el.scrollTop = el.scrollHeight;
2785
+ }
2786
+
2787
+ function renderStudioResult() {
2788
+ var el = document.getElementById('studioResult');
2789
+ if (!el) return;
2790
+ if (!studioState.result) { el.style.display = 'none'; return; }
2791
+ el.style.display = 'block';
2792
+ el.innerHTML = '<div class="studio-result__title">&#10003; Final Result</div><div class="studio-result__body md-body">' + renderMd(studioState.result) + '</div>';
2793
+ }
2794
+
2795
+ function studioSetNodeStatus(idx, status) {
2796
+ if (studioState.nodes[idx]) {
2797
+ studioState.nodes[idx].status = status;
2798
+ renderStudioNodes();
2799
+ }
2800
+ }
2801
+
2802
+ async function runStudio() {
2803
+ var ta = document.getElementById('studioTaskInput');
2804
+ var task = ta ? ta.value.trim() : '';
2805
+ if (!task || studioState.running) return;
2806
+
2807
+ // Reset state
2808
+ studioState.task = task;
2809
+ studioState.nodes = [];
2810
+ studioState.log = [];
2811
+ studioState.result = '';
2812
+ studioState.running = true;
2813
+ studioState.planned = false;
2814
+ renderStudioNodes();
2815
+ renderStudioLog();
2816
+ renderStudioResult();
2817
+
2818
+ var btn = document.getElementById('studioRunBtn');
2819
+ if (btn) { btn.disabled = true; btn.textContent = 'Planning...'; }
2820
+
2821
+ studioLog('Studio', '&#9881;', 'Planning workflow for: "' + task + '"', 'system');
2822
+ // Show a temporary planning indicator in the nodes area
2823
+ var nodesEl = document.getElementById('studioNodes');
2824
+ if (nodesEl) nodesEl.innerHTML = '<div style="text-align:center;padding:20px;color:var(--dim);font-size:12px;font-family:var(--font)"><span class="thinking-dots"><span></span><span></span><span></span></span><div style="margin-top:8px">Designing workflow...</div></div>';
2825
+
2826
+ try {
2827
+ // Step 1: plan the workflow
2828
+ var planRes = await apiPost('/api/studio/plan', {task: task});
2829
+ if (!planRes || !planRes.steps || !planRes.steps.length) {
2830
+ studioLog('Studio', '&#9888;', 'Could not plan workflow. Check your LLM provider config.', 'error');
2831
+ studioState.running = false;
2832
+ if (btn) btn.disabled = false;
2833
+ return;
2834
+ }
2835
+
2836
+ studioState.nodes = planRes.steps.map(function(s) {
2837
+ return {icon: s.icon, agent: s.agent, label: s.label, status: 'waiting'};
2838
+ });
2839
+ renderStudioNodes();
2840
+ studioLog('Studio', '&#10003;', 'Workflow planned: ' + planRes.steps.map(function(s){return s.label}).join(' -> '), 'system');
2841
+
2842
+ // Step 2: execute each step via SSE
2843
+ var context = '';
2844
+ for (var i = 0; i < studioState.nodes.length; i++) {
2845
+ var node = studioState.nodes[i];
2846
+ studioSetNodeStatus(i, 'running');
2847
+ studioLog(node.label, node.icon, 'Starting...', 'agent');
2848
+
2849
+ var stepResult = await runStudioStep(i, node, task, context, planRes.steps[i]);
2850
+ if (stepResult.error) {
2851
+ studioSetNodeStatus(i, 'error');
2852
+ studioLog(node.label, node.icon, 'Error: ' + stepResult.error, 'error');
2853
+ break;
2854
+ }
2855
+ studioSetNodeStatus(i, 'done');
2856
+ studioLog(node.label, node.icon, stepResult.output || '(done)', 'agent', true);
2857
+ // If CanvasAgent produced HTML, open it in the canvas panel
2858
+ if (stepResult.canvas) {
2859
+ var cf = document.getElementById('canvasFrame');
2860
+ var cp = document.getElementById('canvasPanel');
2861
+ var ct = document.getElementById('canvasTitle');
2862
+ if (cf && cp) {
2863
+ cf.srcdoc = stepResult.canvas;
2864
+ cp.classList.add('open');
2865
+ if (ct) ct.textContent = node.label + ' Report';
2866
+ }
2867
+ }
2868
+ context = stepResult.output || stepResult.canvas || context;
2869
+ }
2870
+
2871
+ // Final result is the last step's output
2872
+ studioState.result = context;
2873
+ renderStudioResult();
2874
+ studioLog('Studio', '&#127881;', 'Workflow complete.', 'system');
2875
+
2876
+ // Save session to localStorage for reuse in Chat
2877
+ saveStudioSession(task, studioState.nodes, studioState.log, context);
2878
+ renderStudioSessionsBar();
2879
+
2880
+ } catch(e) {
2881
+ studioLog('Studio', '&#9888;', 'Unexpected error: ' + (e.message || String(e)), 'error');
2882
+ }
2883
+
2884
+ studioState.running = false;
2885
+ if (btn) { btn.disabled = false; btn.textContent = 'Run'; }
2886
+ }
2887
+
2888
+ // ---- STUDIO SESSIONS ----
2889
+ function saveStudioSession(task, nodes, log, result) {
2890
+ try {
2891
+ var sessions = JSON.parse(localStorage.getItem('nha_studio_sessions') || '[]');
2892
+ sessions.unshift({
2893
+ id: Date.now(),
2894
+ task: task,
2895
+ nodes: nodes.map(function(n){return {label:n.label,icon:n.icon,agent:n.agent};}),
2896
+ result: result,
2897
+ log: log.map(function(e){return {agent:e.agent,icon:e.icon,text:e.text,type:e.type,time:e.time};}),
2898
+ ts: new Date().toLocaleString()
2899
+ });
2900
+ sessions = sessions.slice(0, 20); // keep last 20
2901
+ localStorage.setItem('nha_studio_sessions', JSON.stringify(sessions));
2902
+ } catch(e) {}
2903
+ }
2904
+
2905
+ function loadStudioSessions() {
2906
+ try { return JSON.parse(localStorage.getItem('nha_studio_sessions') || '[]'); }
2907
+ catch(e) { return []; }
2908
+ }
2909
+
2910
+ function renderStudioSessionsBar() {
2911
+ var el = document.getElementById('studioSessionsBar');
2912
+ if (!el) return;
2913
+ var sessions = loadStudioSessions();
2914
+ if (!sessions.length) { el.style.display = 'none'; return; }
2915
+ el.style.display = 'block';
2916
+ el.innerHTML = '<div style="font-size:10px;color:var(--dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:1px">Recent sessions</div>' +
2917
+ sessions.slice(0,5).map(function(s,i) {
2918
+ return '<div class="studio-session-item">' +
2919
+ '<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">' +
2920
+ '<span class="studio-session-task">' + esc(s.task.slice(0,60)) + (s.task.length>60?'...':'') + '</span>' +
2921
+ '<span style="font-size:9px;color:var(--dim);white-space:nowrap">' + esc(s.ts) + '</span>' +
2922
+ '</div>' +
2923
+ '<div style="display:flex;gap:6px;margin-top:6px">' +
2924
+ '<button onclick="restoreStudioSession('+i+')" style="font-size:10px;padding:3px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:4px;color:var(--green);cursor:pointer">Restore</button>' +
2925
+ '<button onclick="importStudioToChat('+i+')" style="font-size:10px;padding:3px 8px;background:var(--bg3);border:1px solid var(--border2);border-radius:4px;color:var(--cyan);cursor:pointer">Send to Chat</button>' +
2926
+ '<button onclick="deleteStudioSession('+i+')" style="font-size:10px;padding:3px 8px;background:none;border:none;color:var(--dim);cursor:pointer">&times;</button>' +
2927
+ '</div>' +
2928
+ '</div>';
2929
+ }).join('');
2930
+ }
2931
+
2932
+ function restoreStudioSession(idx) {
2933
+ var sessions = loadStudioSessions();
2934
+ var s = sessions[idx]; if (!s) return;
2935
+ studioState.task = s.task;
2936
+ studioState.nodes = s.nodes.map(function(n){return {icon:n.icon,agent:n.agent,label:n.label,status:'done'};});
2937
+ studioState.log = s.log;
2938
+ studioState.result = s.result;
2939
+ studioState.running = false;
2940
+ var ta = document.getElementById('studioTaskInput');
2941
+ if (ta) ta.value = s.task;
2942
+ renderStudioNodes(); renderStudioLog(); renderStudioResult();
2943
+ toast('Session restored');
2944
+ }
2945
+
2946
+ function importStudioToChat(idx) {
2947
+ var sessions = loadStudioSessions();
2948
+ var s = sessions[idx]; if (!s) return;
2949
+ // Build a context message and inject into chat
2950
+ var NL2 = '\\n\\n';
2951
+ var summary = '**Studio Workflow Result**' + NL2 + 'Task: ' + s.task + NL2 + 'Agents: ' + s.nodes.map(function(n){return n.label;}).join(' -> ') + NL2 + '---' + NL2 + (s.result || '');
2952
+ // Switch to chat and pre-fill with context
2953
+ switchView('chat');
2954
+ setTimeout(function() {
2955
+ createNewConv().then(function() {
2956
+ // Add the studio result as an assistant message in history
2957
+ chatHistory = [{role:'assistant', content: summary}];
2958
+ renderMessages();
2959
+ var inp = document.getElementById('chatInput');
2960
+ if (inp) { inp.focus(); inp.placeholder = 'Ask follow-up questions about this studio result...'; }
2961
+ toast('Studio result imported into chat. Ask your follow-up question.');
2962
+ });
2963
+ }, 300);
2964
+ }
2965
+
2966
+ function deleteStudioSession(idx) {
2967
+ try {
2968
+ var sessions = JSON.parse(localStorage.getItem('nha_studio_sessions') || '[]');
2969
+ sessions.splice(idx, 1);
2970
+ localStorage.setItem('nha_studio_sessions', JSON.stringify(sessions));
2971
+ renderStudioSessionsBar();
2972
+ } catch(e) {}
2973
+ }
2974
+
2975
+ // ---- TOKEN COUNTER for Studio ----
2976
+ var studioTokens = {in: 0, out: 0};
2977
+ function studioAddTokens(inp, out) {
2978
+ studioTokens.in += (inp||0);
2979
+ studioTokens.out += (out||0);
2980
+ var el = document.getElementById('studioTokenBar');
2981
+ if (el) el.textContent = 'Tokens: ' + studioTokens.in + ' in / ' + studioTokens.out + ' out';
2982
+ }
2983
+
2984
+ function runStudioStep(idx, node, task, context, stepDef) {
2985
+ return new Promise(function(resolve) {
2986
+ var output = '';
2987
+ var canvasHtml = null;
2988
+ var body = JSON.stringify({stepIdx: idx, agent: node.agent, task: task, context: context, stepDef: stepDef});
2989
+
2990
+ fetch('/api/studio/run', {
2991
+ method: 'POST',
2992
+ headers: {'Content-Type': 'application/json'},
2993
+ body: body
2994
+ }).then(function(res) {
2995
+ if (!res.ok) { resolve({error: 'HTTP ' + res.status}); return; }
2996
+ var reader = res.body.getReader();
2997
+ var decoder = new TextDecoder();
2998
+ var buf = '';
2999
+ function pump() {
3000
+ reader.read().then(function(chunk) {
3001
+ if (chunk.done) { resolve({output: output || '(no output)', canvas: canvasHtml}); return; }
3002
+ buf += decoder.decode(chunk.value, {stream: true});
3003
+ var lines = buf.split('\\n');
3004
+ buf = lines.pop();
3005
+ lines.forEach(function(line) {
3006
+ if (!line.startsWith('data: ')) return;
3007
+ var d = line.slice(6).trim();
3008
+ if (d === '[DONE]') { resolve({output: output || '(no output)', canvas: canvasHtml}); return; }
3009
+ try {
3010
+ var ev = JSON.parse(d);
3011
+ if (ev.token) {
3012
+ // Tool status tokens (start with '[') shown in dim color, LLM output streamed normally
3013
+ var isStatus = ev.token.charAt(0) === '[' && ev.token.indexOf(']') > 0 && ev.token.length < 60;
3014
+ if (!isStatus) output += ev.token;
3015
+ // Update live log entry
3016
+ var entries = document.querySelectorAll('.studio-log-entry');
3017
+ var last = entries[entries.length - 1];
3018
+ if (last) {
3019
+ var tb = last.querySelector('.studio-log-entry__text');
3020
+ if (tb) tb.textContent = isStatus ? ev.token.replace(/[\\r\\n]/g, '') : output;
3021
+ }
3022
+ }
3023
+ if (ev.canvas) {
3024
+ canvasHtml = ev.canvas;
3025
+ // Render canvas immediately when received — don't wait for step resolution
3026
+ var cf2 = document.getElementById('canvasFrame');
3027
+ var cp2 = document.getElementById('canvasPanel');
3028
+ if (cf2 && cp2) {
3029
+ cf2.srcdoc = canvasHtml;
3030
+ cp2.classList.add('open');
3031
+ var ct2 = document.getElementById('canvasTitle');
3032
+ if (ct2) ct2.textContent = 'Studio Report';
3033
+ }
3034
+ }
3035
+ if (ev.usage) { studioAddTokens(ev.usage.input||0, ev.usage.output||0); }
3036
+ if (ev.done) { resolve({output: output || '(no output)', canvas: canvasHtml}); return; }
3037
+ if (ev.error) { resolve({error: ev.error}); return; }
3038
+ } catch(e) {}
3039
+ });
3040
+ pump();
3041
+ }).catch(function(e) { resolve({error: e.message}); });
3042
+ }
3043
+ pump();
3044
+ }).catch(function(e) { resolve({error: e.message}); });
3045
+ });
3046
+ }
3047
+
3048
+ function renderStudio(el) {
3049
+ var examplesHtml = STUDIO_EXAMPLES.map(function(ex) {
3050
+ return '<button class="studio-example-btn" onclick="document.getElementById(\\'studioTaskInput\\').value=' + JSON.stringify(ex) + '">' + esc(ex.slice(0, 52)) + (ex.length > 52 ? '...' : '') + '</button>';
3051
+ }).join('');
3052
+
3053
+ // Agent catalog
3054
+ var STUDIO_AGENTS = [
3055
+ {icon:'&#128269;',name:'WebSearchAgent',desc:'Search the web'},
3056
+ {icon:'&#127760;',name:'BrowserAgent',desc:'Navigate & screenshot pages'},
3057
+ {icon:'&#128140;',name:'EmailAgent',desc:'Read & summarize emails'},
3058
+ {icon:'&#128197;',name:'CalendarAgent',desc:'Read events & scheduling'},
3059
+ {icon:'&#9881;',name:'GitHubAgent',desc:'Issues, PRs, notifications'},
3060
+ {icon:'&#128214;',name:'NotionAgent',desc:'Search Notion pages'},
3061
+ {icon:'&#128172;',name:'SlackAgent',desc:'Read channels'},
3062
+ {icon:'&#128202;',name:'DataAnalystAgent',desc:'Analyze data & patterns'},
3063
+ {icon:'&#9999;',name:'WriterAgent',desc:'Write structured documents'},
3064
+ {icon:'&#128203;',name:'SummaryAgent',desc:'Summarize long content'},
3065
+ {icon:'&#128270;',name:'ResearchAgent',desc:'Deep research & facts'},
3066
+ {icon:'&#128247;',name:'CanvasAgent',desc:'Generate HTML visual report'},
3067
+ {icon:'&#128274;',name:'SecurityAgent',desc:'Security analysis'},
3068
+ {icon:'&#128295;',name:'DevOpsAgent',desc:'Infrastructure analysis'},
3069
+ ];
3070
+
3071
+ var toolsHtml = STUDIO_AGENTS.map(function(t){
3072
+ return '<div class="studio-tool-item" onclick="addAgentToBuilder(\\x27'+t.name+'\\x27,\\x27'+t.icon+'\\x27)">' +
3073
+ '<span class="studio-tool-icon">'+t.icon+'</span>' +
3074
+ '<div><div style="font-size:11px;font-weight:600;color:var(--green)">'+t.name+'</div><div style="font-size:10px;color:var(--dim)">'+esc(t.desc)+'</div></div>' +
3075
+ '<span style="margin-left:auto;font-size:14px;color:var(--green3);flex-shrink:0">+</span>' +
3076
+ '</div>';
3077
+ }).join('');
3078
+
3079
+ // 38 specialist agents sidebar
3080
+ var SPECIALIST_AGENTS = [
3081
+ {icon:'\\u{1F6E1}',name:'saber',desc:'Security audits, pentest, OWASP'},
3082
+ {icon:'\\u{1F50D}',name:'zero',desc:'Vulnerability & dependency audit'},
3083
+ {icon:'\\u2713',name:'veritas',desc:'Fact-checking & hallucination detection'},
3084
+ {icon:'\\u{1F52C}',name:'ade',desc:'Full security review, forensics'},
3085
+ {icon:'\\u{1F512}',name:'heimdall',desc:'OAuth, JWT, RBAC design'},
3086
+ {icon:'\\u{1F4BB}',name:'jarvis',desc:'Full-stack architecture & API'},
3087
+ {icon:'\\u2699',name:'forge',desc:'CI/CD, deployment, infra'},
3088
+ {icon:'\\u{1F527}',name:'pipe',desc:'Build systems, Airflow, automation'},
3089
+ {icon:'\\u{1F4DF}',name:'shell',desc:'Shell scripts, CLI tools'},
3090
+ {icon:'\\u{1F41B}',name:'glitch',desc:'Debugging & root cause'},
3091
+ {icon:'\\u{1F4CA}',name:'oracle',desc:'Data analysis, stats, ML'},
3092
+ {icon:'\\u{1F9EE}',name:'logos',desc:'Logic, proofs, formal reasoning'},
3093
+ {icon:'\\u{1F5FA}',name:'atlas',desc:'Terraform, CloudFormation, IaC'},
3094
+ {icon:'\\u{1F30D}',name:'cartographer',desc:'Geo data, mapping, routing'},
3095
+ {icon:'\\u270D',name:'scheherazade',desc:'Docs, tutorials, blog posts'},
3096
+ {icon:'\\u{1F4DD}',name:'quill',desc:'Posts, summaries, abstracts'},
3097
+ {icon:'\\u{1F3A8}',name:'muse',desc:'Creative brainstorming & ideation'},
3098
+ {icon:'\\u{1F58C}',name:'murasaki',desc:'UI/UX design, accessibility'},
3099
+ {icon:'\\u{1F517}',name:'hermes',desc:'Kafka, RabbitMQ, event-driven'},
3100
+ {icon:'\\u{1F50C}',name:'link',desc:'Community, reputation, engagement'},
3101
+ {icon:'\\u{1F310}',name:'mercury',desc:'Finance, market, ROI analysis'},
3102
+ {icon:'\\u2638',name:'shogun',desc:'Kubernetes, Helm, pod security'},
3103
+ {icon:'\\u{1F504}',name:'flux',desc:'GitOps, rollback planning'},
3104
+ {icon:'\\u23F0',name:'cron',desc:'GitHub Actions, GitLab CI'},
3105
+ {icon:'\\u{1F30E}',name:'babel',desc:'API integration, microservices'},
3106
+ {icon:'\\u{1F5E3}',name:'polyglot',desc:'i18n, localization, translation'},
3107
+ {icon:'\\u{1F4E2}',name:'herald',desc:'News analysis, trend detection'},
3108
+ {icon:'\\u{1F4E1}',name:'echo',desc:'Content repurposing: blog→social'},
3109
+ {icon:'\\u26A1',name:'macro',desc:'Batch processing, data migration'},
3110
+ {icon:'\\u{1F525}',name:'prometheus',desc:'Strategy, architecture trade-offs'},
3111
+ {icon:'\\u26A0',name:'cassandra',desc:'Risk prediction, worst-case analysis'},
3112
+ {icon:'\\u{1F9E0}',name:'athena',desc:'Tech evaluation, benchmarks'},
3113
+ {icon:'\\u{1F441}',name:'sauron',desc:'Performance profiling, bottlenecks'},
3114
+ {icon:'\\u{1F3BC}',name:'conductor',desc:'Workflow orchestration'},
3115
+ {icon:'\\u{1F9ED}',name:'navi',desc:'Data profiling, schema inference'},
3116
+ {icon:'\\u{1F4C8}',name:'edi',desc:'A/B testing, hypothesis testing'},
3117
+ {icon:'\\u26C8',name:'tempest',desc:'Climate, weather, environmental'},
3118
+ {icon:'\\u{1F37D}',name:'epicure',desc:'Recipes, nutrition, dietary'},
3119
+ ];
3120
+ var specialistHtml = SPECIALIST_AGENTS.map(function(t){
3121
+ var ic = t.icon;
3122
+ return '<div class="studio-tool-item" onclick="addAgentToBuilder(\\x27'+t.name+'\\x27,\\x27'+ic+'\\x27)">' +
3123
+ '<span class="studio-tool-icon">'+ic+'</span>' +
3124
+ '<div><div style="font-size:11px;font-weight:600;color:var(--cyan)">'+t.name+'</div><div style="font-size:10px;color:var(--dim)">'+esc(t.desc)+'</div></div>' +
3125
+ '<span style="margin-left:auto;font-size:14px;color:var(--green3);flex-shrink:0">+</span>' +
3126
+ '</div>';
3127
+ }).join('');
3128
+
3129
+ el.innerHTML =
3130
+ '<div class="studio-header">' +
3131
+ '<h2>&#9881; NHA Studio</h2>' +
3132
+ '<p>Build a pipeline manually — click agents to add them in order — or describe your task in natural language and let Studio plan it automatically.</p>' +
3133
+ '</div>' +
3134
+ '<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start">' +
3135
+ '<div style="flex:1;min-width:0">' +
3136
+
3137
+ // ── MODE TABS ──
3138
+ '<div style="display:flex;gap:0;margin-bottom:14px;border:1px solid var(--border);border-radius:8px;overflow:hidden">' +
3139
+ '<button id="studioTabAuto" onclick="studioSetMode(\\x27auto\\x27)" style="flex:1;padding:8px;font-size:11px;font-weight:600;background:var(--green3);color:#fff;border:none;cursor:pointer">&#128161; Auto Plan</button>' +
3140
+ '<button id="studioTabManual" onclick="studioSetMode(\\x27manual\\x27)" style="flex:1;padding:8px;font-size:11px;font-weight:600;background:var(--bg3);color:var(--dim);border:none;cursor:pointer">&#128295; Manual Builder</button>' +
3141
+ '</div>' +
3142
+
3143
+ // ── AUTO MODE ──
3144
+ '<div id="studioAutoMode">' +
3145
+ '<div style="margin-bottom:10px">' +
3146
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:6px;text-transform:uppercase;letter-spacing:1px">Examples</div>' +
3147
+ examplesHtml +
3148
+ '</div>' +
3149
+ '<div class="studio-input-row">' +
3150
+ '<textarea id="studioTaskInput" placeholder="Describe what you want to accomplish... (Ctrl+Enter to run)" onkeydown="if(event.key===\\x27Enter\\x27&&(event.ctrlKey||event.metaKey)){runStudio();event.preventDefault()}">' + esc(studioState.task) + '</textarea>' +
3151
+ '<button id="studioRunBtn" class="studio-run-btn" onclick="runStudio()" ' + (studioState.running ? 'disabled' : '') + '>&#9654; Run</button>' +
3152
+ '</div>' +
3153
+ '</div>' +
3154
+
3155
+ // ── MANUAL BUILDER MODE ──
3156
+ '<div id="studioManualMode" style="display:none">' +
3157
+ '<div style="margin-bottom:10px">' +
3158
+ '<input id="studioManualTask" placeholder="Describe the overall goal (optional)..." style="width:100%;padding:10px 14px;font-size:13px;border-radius:8px;border:1px solid var(--border);background:var(--bg2);color:var(--text)">' +
3159
+ '</div>' +
3160
+ '<div id="studioBuilderPipeline" class="studio-builder-pipeline">' +
3161
+ '<div class="studio-builder-empty">Click agents on the right to build your pipeline</div>' +
3162
+ '</div>' +
3163
+ '<div style="display:flex;gap:8px;margin-top:10px">' +
3164
+ '<button onclick="runManualWorkflow()" class="studio-run-btn" style="flex:1">&#9654; Run Pipeline</button>' +
3165
+ '<button onclick="clearBuilderPipeline()" style="padding:8px 14px;background:none;border:1px solid var(--border);border-radius:8px;color:var(--dim);cursor:pointer;font-size:12px">Clear</button>' +
3166
+ '</div>' +
3167
+ '</div>' +
3168
+
3169
+ '<div id="studioTokenBar" style="font-size:10px;color:var(--dim);margin:8px 0;font-family:var(--mono)"></div>' +
3170
+ '<div class="studio-canvas" id="studioNodes"></div>' +
3171
+ '<div class="studio-log" id="studioLog" style="display:none"></div>' +
3172
+ '<div id="studioResult"></div>' +
3173
+ '<div id="studioSessionsBar" style="margin-top:16px;display:none"></div>' +
3174
+ '</div>' +
3175
+
3176
+ // ── AGENT SIDEBAR ──
3177
+ '<div class="studio-tools-panel">' +
3178
+ // Tab bar
3179
+ '<div style="display:flex;gap:0;margin-bottom:10px;border:1px solid var(--border);border-radius:6px;overflow:hidden">' +
3180
+ '<button id="sideTabTools" onclick="studioSideTab(\\x27tools\\x27)" style="flex:1;padding:5px;font-size:10px;font-weight:600;background:var(--green3);color:#fff;border:none;cursor:pointer">&#128295; Tools</button>' +
3181
+ '<button id="sideTabAgents" onclick="studioSideTab(\\x27agents\\x27)" style="flex:1;padding:5px;font-size:10px;font-weight:600;background:var(--bg3);color:var(--dim);border:none;cursor:pointer">&#129302; Agents</button>' +
3182
+ '</div>' +
3183
+ '<div style="font-size:9px;color:var(--dim);margin-bottom:8px">Click to add to pipeline</div>' +
3184
+ '<div id="sideToolsList">'+toolsHtml+'</div>' +
3185
+ '<div id="sideAgentsList" style="display:none">'+specialistHtml+'</div>' +
3186
+ '</div>' +
3187
+ '</div>';
3188
+
3189
+ renderStudioNodes();
3190
+ renderStudioLog();
3191
+ renderStudioResult();
3192
+ renderStudioSessionsBar();
3193
+ studioTokens = {in:0,out:0};
3194
+ // Restore pipeline from state
3195
+ renderBuilderPipeline();
3196
+ }
3197
+
3198
+ // ---- STUDIO SIDEBAR TAB ----
3199
+ function studioSideTab(tab) {
3200
+ var toolsList = document.getElementById('sideToolsList');
3201
+ var agentsList = document.getElementById('sideAgentsList');
3202
+ var tabT = document.getElementById('sideTabTools');
3203
+ var tabA = document.getElementById('sideTabAgents');
3204
+ if (!toolsList || !agentsList) return;
3205
+ if (tab === 'tools') {
3206
+ toolsList.style.display = '';
3207
+ agentsList.style.display = 'none';
3208
+ if (tabT) { tabT.style.background='var(--green3)'; tabT.style.color='#fff'; }
3209
+ if (tabA) { tabA.style.background='var(--bg3)'; tabA.style.color='var(--dim)'; }
3210
+ } else {
3211
+ toolsList.style.display = 'none';
3212
+ agentsList.style.display = '';
3213
+ if (tabA) { tabA.style.background='var(--cyan)'; tabA.style.color='#fff'; }
3214
+ if (tabT) { tabT.style.background='var(--bg3)'; tabT.style.color='var(--dim)'; }
3215
+ }
3216
+ }
3217
+
3218
+ // ---- STUDIO MODE SWITCHING ----
3219
+ var studioMode = 'auto';
3220
+ var builderPipeline = []; // [{icon,name,agent}]
3221
+
3222
+ function studioSetMode(mode) {
3223
+ studioMode = mode;
3224
+ var autoEl = document.getElementById('studioAutoMode');
3225
+ var manEl = document.getElementById('studioManualMode');
3226
+ var tabA = document.getElementById('studioTabAuto');
3227
+ var tabM = document.getElementById('studioTabManual');
3228
+ if (!autoEl || !manEl) return;
3229
+ if (mode === 'auto') {
3230
+ autoEl.style.display = '';
3231
+ manEl.style.display = 'none';
3232
+ if (tabA) { tabA.style.background='var(--green3)'; tabA.style.color='#fff'; }
3233
+ if (tabM) { tabM.style.background='var(--bg3)'; tabM.style.color='var(--dim)'; }
3234
+ } else {
3235
+ autoEl.style.display = 'none';
3236
+ manEl.style.display = '';
3237
+ if (tabM) { tabM.style.background='var(--green3)'; tabM.style.color='#fff'; }
3238
+ if (tabA) { tabA.style.background='var(--bg3)'; tabA.style.color='var(--dim)'; }
3239
+ renderBuilderPipeline();
3240
+ }
3241
+ }
3242
+
3243
+ // ---- MANUAL BUILDER ----
3244
+ function addAgentToBuilder(agentName, icon) {
3245
+ if (builderPipeline.length >= 6) { toast('Max 6 agents per pipeline'); return; }
3246
+ builderPipeline.push({icon: icon||'&#9881;', name: agentName, agent: agentName, label: agentName.replace('Agent',''), status:'waiting'});
3247
+ // Auto-switch to manual mode when user clicks an agent
3248
+ studioSetMode('manual');
3249
+ renderBuilderPipeline();
3250
+ }
3251
+
3252
+ function removeBuilderAgent(idx) {
3253
+ builderPipeline.splice(idx, 1);
3254
+ renderBuilderPipeline();
3255
+ }
3256
+
3257
+ function moveBuilderAgent(idx, dir) {
3258
+ var newIdx = idx + dir;
3259
+ if (newIdx < 0 || newIdx >= builderPipeline.length) return;
3260
+ var tmp = builderPipeline[idx];
3261
+ builderPipeline[idx] = builderPipeline[newIdx];
3262
+ builderPipeline[newIdx] = tmp;
3263
+ renderBuilderPipeline();
3264
+ }
3265
+
3266
+ function clearBuilderPipeline() {
3267
+ builderPipeline = [];
3268
+ renderBuilderPipeline();
3269
+ }
3270
+
3271
+ function renderBuilderPipeline() {
3272
+ var el = document.getElementById('studioBuilderPipeline');
3273
+ if (!el) return;
3274
+ if (!builderPipeline.length) {
3275
+ el.innerHTML = '<div class="studio-builder-empty">Click agents on the right to build your pipeline</div>';
3276
+ return;
3277
+ }
3278
+ el.innerHTML = builderPipeline.map(function(a, i) {
3279
+ return '<div class="studio-builder-node" draggable="true" ondragstart="builderDragStart('+i+')" ondragover="event.preventDefault()" ondrop="builderDrop('+i+')">' +
3280
+ '<span style="font-size:18px;flex-shrink:0">'+a.icon+'</span>' +
3281
+ '<div style="flex:1;min-width:0">' +
3282
+ '<div style="font-size:12px;font-weight:600;color:var(--green)">'+esc(a.label||a.name)+'</div>' +
3283
+ '<div style="font-size:10px;color:var(--dim)">Step '+(i+1)+'</div>' +
3284
+ '</div>' +
3285
+ '<div style="display:flex;gap:2px;align-items:center">' +
3286
+ (i>0?'<button onclick="moveBuilderAgent('+i+',-1)" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:12px;padding:2px 4px" title="Move up">&#8593;</button>':'') +
3287
+ (i<builderPipeline.length-1?'<button onclick="moveBuilderAgent('+i+',1)" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:12px;padding:2px 4px" title="Move down">&#8595;</button>':'') +
3288
+ '<button onclick="removeBuilderAgent('+i+')" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px;padding:2px 4px" title="Remove">&times;</button>' +
3289
+ '</div>' +
3290
+ (i<builderPipeline.length-1?'<div style="position:absolute;bottom:-14px;left:50%;transform:translateX(-50%);color:var(--green3);font-size:12px">&#8595;</div>':'') +
3291
+ '</div>';
3292
+ }).join('');
3293
+ }
3294
+
3295
+ // Drag-and-drop reorder for builder pipeline
3296
+ var builderDragIdx = -1;
3297
+ function builderDragStart(idx) { builderDragIdx = idx; }
3298
+ function builderDrop(toIdx) {
3299
+ if (builderDragIdx < 0 || builderDragIdx === toIdx) return;
3300
+ var item = builderPipeline.splice(builderDragIdx, 1)[0];
3301
+ builderPipeline.splice(toIdx, 0, item);
3302
+ builderDragIdx = -1;
3303
+ renderBuilderPipeline();
3304
+ }
3305
+
3306
+ async function runManualWorkflow() {
3307
+ if (!builderPipeline.length) { toast('Add at least one agent to the pipeline'); return; }
3308
+ if (studioState.running) return;
3309
+
3310
+ var taskEl = document.getElementById('studioManualTask');
3311
+ var task = taskEl ? taskEl.value.trim() : '';
3312
+ if (!task) task = builderPipeline.map(function(a){return a.label||a.name;}).join(' -> ') + ' pipeline';
3313
+
3314
+ // Build steps in the same format runStudio expects, skipping the LLM planner
3315
+ var steps = builderPipeline.map(function(a) {
3316
+ return {
3317
+ agent: a.agent,
3318
+ label: a.label || a.name,
3319
+ icon: a.icon,
3320
+ prompt: task,
3321
+ status: 'waiting'
3322
+ };
3323
+ });
3324
+
3325
+ studioState.task = task;
3326
+ studioState.nodes = steps.map(function(s){return {icon:s.icon,agent:s.agent,label:s.label,status:'waiting'};});
3327
+ studioState.log = [];
3328
+ studioState.result = '';
3329
+ studioState.running = true;
3330
+ studioTokens = {in:0, out:0};
3331
+ var tb = document.getElementById('studioTokenBar');
3332
+ if(tb) tb.textContent = '';
3333
+ renderStudioNodes();
3334
+ renderStudioLog();
3335
+ renderStudioResult();
3336
+
3337
+ var context = '';
3338
+ try {
3339
+ for (var i = 0; i < steps.length; i++) {
3340
+ var s = steps[i];
3341
+ studioSetNodeStatus(i, 'running');
3342
+ studioLog(s.label, s.icon, 'Starting...', 'agent');
3343
+ var stepResult = await runStudioStep(i, {agent:s.agent,label:s.label,icon:s.icon}, task, context, s);
3344
+ if (stepResult.error) {
3345
+ studioSetNodeStatus(i, 'error');
3346
+ studioLog(s.label, s.icon, 'Error: '+stepResult.error, 'error');
3347
+ break;
3348
+ }
3349
+ studioSetNodeStatus(i, 'done');
3350
+ studioLog(s.label, s.icon, stepResult.output || '(done)', 'agent', true);
3351
+ if (stepResult.canvas) {
3352
+ var cf = document.getElementById('canvasFrame');
3353
+ var cp = document.getElementById('canvasPanel');
3354
+ if (cf && cp) { cf.srcdoc = stepResult.canvas; cp.classList.add('open'); var ct3=document.getElementById('canvasTitle'); if(ct3) ct3.textContent='Studio Report'; }
3355
+ }
3356
+ context = stepResult.output || stepResult.canvas || context;
3357
+ }
3358
+ studioState.result = context;
3359
+ renderStudioResult();
3360
+ studioLog('Studio', '&#127881;', 'Pipeline complete.', 'system');
3361
+ saveStudioSession(task, studioState.nodes, studioState.log, context);
3362
+ renderStudioSessionsBar();
3363
+ } catch(e) {
3364
+ studioLog('Studio', '&#9888;', 'Error: '+(e.message||String(e)), 'error');
3365
+ }
3366
+ studioState.running = false;
3367
+ }
3368
+
2772
3369
  // ---- INIT ----
2773
3370
  function init(){
2774
3371
  var el=document.getElementById('content');
@@ -2778,8 +3375,373 @@ function init(){
2778
3375
  setInterval(function(){loadDash().then(function(){if(currentView==='dashboard')render()}).catch(function(){})},120000);
2779
3376
  // Connect to daemon WebSocket for real-time notifications
2780
3377
  connectWebSocket();
3378
+ // Make floating panels draggable
3379
+ var bv=document.getElementById('browserViewer');
3380
+ if(bv)makeDraggable(bv,'.browser-viewer__header');
3381
+ var cp=document.getElementById('canvasPanel');
3382
+ if(cp)makeDraggable(cp,'.cvs-header');
2781
3383
  }
2782
3384
  init();
3385
+ `;
3386
+
3387
+ export function getHTML(port) {
3388
+ const ts = Date.now();
3389
+
3390
+ // CSS as a clean block — no template literal nesting issues
3391
+ const CSS = `
3392
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
3393
+ :root{
3394
+ --bg:#0a0a0a;--bg2:#111;--bg3:#1a1a2e;--bg4:#222;
3395
+ --green:#a5b4fc;--green2:#818cf8;--green3:#6366f1;--greendim:#12121e;
3396
+ --cyan:#38bdf8;--amber:#fbbf24;--red:#ef4444;
3397
+ --text:#e4e4e7;--dim:#9ca3af;--bright:#fff;
3398
+ --border:#1e1e2e;--border2:#3f3f5e;
3399
+ --font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
3400
+ --mono:'JetBrains Mono','Fira Code','SF Mono','Consolas',monospace;
3401
+ --term:'JetBrains Mono','Fira Code','SF Mono','Consolas',monospace;
3402
+ --amber3:#d97706;--amberdim:#1a1200;
3403
+ --r:6px;
3404
+ }
3405
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);font-size:13.5px;line-height:1.6}
3406
+ a{color:var(--cyan);text-decoration:none}
3407
+ button{font-family:var(--font);cursor:pointer;border:none;outline:none}
3408
+ input,textarea{font-family:var(--font);background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:var(--r);outline:none;font-size:13px}
3409
+ input:focus,textarea:focus{border-color:var(--green3)}
3410
+ ::-webkit-scrollbar{width:6px}
3411
+ ::-webkit-scrollbar-track{background:var(--bg)}
3412
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
3413
+
3414
+ /* ---- TASKS ---- */
3415
+ .task-bar{display:flex;gap:8px;margin-bottom:16px}
3416
+ .task-bar input{flex:1;font-size:13px;padding:10px 14px}
3417
+ .task-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--r);font-size:12px}
3418
+ .task-bar button{background:var(--green3);color:var(--bg);padding:8px 20px;border-radius:var(--r);font-weight:700;font-size:13px}
3419
+ .task{display:flex;align-items:center;gap:10px;padding:12px;cursor:pointer}
3420
+ .task--done{opacity:0.5}
3421
+ .task__check{width:24px;height:24px;border:2px solid var(--border2);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--green);cursor:pointer;flex-shrink:0}
3422
+ .task__check--done{background:var(--green3);border-color:var(--green)}
3423
+ .task__desc{flex:1;font-size:13px}
3424
+ .task__priority{font-size:10px;padding:2px 8px;border-radius:4px;text-transform:uppercase}
3425
+ .task__priority--critical{background:var(--red);color:var(--bright)}
3426
+ .task__priority--high{background:#ff6d00;color:var(--bright)}
3427
+ .task__priority--medium{background:var(--amber);color:var(--bg)}
3428
+ .task__priority--low{background:var(--border2);color:var(--dim)}
3429
+
3430
+ /* ---- LAYOUT: mobile-first ---- */
3431
+ .app{display:flex;flex-direction:column;height:100vh;height:100dvh}
3432
+ /* header removed — info moved to sidebar brand */
3433
+
3434
+ .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)}
3435
+ .sidebar--open{display:flex}
3436
+ .sidebar__overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:150}
3437
+ .sidebar__overlay--open{display:block}
3438
+ .sidebar__brand{padding:16px;border-bottom:1px solid var(--border)}
3439
+ .sidebar__brand-name{font-size:16px;color:var(--green);font-weight:700;letter-spacing:2px}
3440
+ .sidebar__brand-sub{font-size:10px;color:var(--dim);margin-top:2px}
3441
+ .sidebar__section{padding:12px 0}
3442
+ .sidebar__label{padding:0 16px;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--dim);margin-bottom:4px}
3443
+ .nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;color:var(--dim);cursor:pointer;font-size:12px;border-left:2px solid transparent}
3444
+ .nav-item:hover,.nav-item--active{color:var(--bright);background:var(--bg3)}
3445
+ .nav-item--active{border-left-color:var(--green);color:var(--green)}
3446
+ .nav-item__icon{width:18px;text-align:center}
3447
+ .nav-item__badge{background:var(--red);color:var(--bright);font-size:9px;padding:1px 5px;border-radius:8px;margin-left:auto}
3448
+
3449
+ .content{flex:1;overflow-y:auto;padding:16px;-webkit-overflow-scrolling:touch}
3450
+
3451
+ /* Mobile burger button */
3452
+ #mobileBurger{display:block}
3453
+ .sidebar__close{position:absolute;top:12px;right:12px;background:none;border:none;color:var(--dim);font-size:20px;cursor:pointer;padding:4px 8px;z-index:10;line-height:1}
3454
+ .sidebar__close:hover{color:var(--bright)}
3455
+ .sidebar__brand{position:relative}
3456
+
3457
+ /* ---- DESKTOP: sidebar always visible ---- */
3458
+ @media(min-width:901px){
3459
+ .app{flex-direction:row}
3460
+ .header__burger{display:none}
3461
+ #mobileBurger{display:none!important}
3462
+ .sidebar__close{display:none}
3463
+ .sidebar{display:flex!important;position:static;width:220px;min-width:220px;height:auto;box-shadow:none}
3464
+ .sidebar__overlay{display:none!important}
3465
+ .content{padding:20px}
3466
+ }
3467
+
3468
+ /* ---- CARDS ---- */
3469
+ .card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:10px}
3470
+ .card__title{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin-bottom:6px}
3471
+ .card__value{font-size:22px;font-weight:700;color:var(--green)}
3472
+ .card__sub{font-size:11px;color:var(--dim);margin-top:3px}
3473
+ .dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
3474
+ @media(min-width:901px){.dash-grid{grid-template-columns:repeat(4,1fr)}}
3475
+ .section-title{font-size:12px;color:var(--cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
3476
+
3477
+ /* ---- CHAT ---- */
3478
+ .content--chat{overflow:hidden!important;padding:0!important;display:flex;flex-direction:column}
3479
+ .chat{display:flex;flex-direction:column;flex:1;min-height:0;padding:16px;padding-bottom:0}
3480
+ @media(min-width:901px){.content--chat{padding:0!important}}
3481
+ .chat__messages{flex:1;overflow-y:auto;padding-bottom:12px;-webkit-overflow-scrolling:touch}
3482
+ .chat__empty{text-align:center;padding:60px 16px;color:var(--dim)}
3483
+ .chat__empty-title{font-size:28px;color:var(--green);margin-bottom:12px}
3484
+ .chat__empty-hint{font-size:11px;margin-top:12px}
3485
+ .msg{margin-bottom:12px}
3486
+ .msg--user .msg__bubble{background:var(--bg3);border:1px solid var(--border2);border-radius:8px 8px 2px 8px;padding:10px 14px;max-width:85%;margin-left:auto;color:var(--bright)}
3487
+ .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;line-height:1.5}
3488
+ .msg--assistant .msg__bubble img{max-width:100%;border-radius:8px;margin:8px 0;border:1px solid rgba(0,255,65,0.2)}
3489
+ .msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
3490
+ .msg__actions{display:flex;gap:6px;margin-top:4px;opacity:0.4;transition:opacity 0.2s}
3491
+ .msg:hover .msg__actions{opacity:1}
3492
+ .msg__actions button{background:none;border:none;color:var(--dim);cursor:pointer;font-size:10px;font-family:var(--mono);padding:2px 4px}
3493
+ .msg__actions button:hover{color:var(--green)}
3494
+ #canvasPanel{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:none;flex-direction:column;box-shadow:0 0 30px rgba(0,255,65,0.1)}
3495
+ #canvasPanel.open{display:flex}
3496
+ #canvasPanel .cvs-header{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)}
3497
+ #canvasPanel .cvs-header span{font-family:var(--mono);color:var(--green);font-size:12px}
3498
+ #canvasPanel .cvs-header button{background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;margin-left:8px}
3499
+ #canvasPanel iframe{flex:1;border:none;background:#fff;min-height:350px;width:100%}
3500
+ .msg--thinking{color:var(--dim);font-style:italic}
3501
+ .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)}
3502
+ .tool-indicator--browser{border-color:#9c27b0;color:#ce93d8}
3503
+ .tool-indicator--web{border-color:var(--cyan);color:var(--cyan)}
3504
+ .tool-indicator--email{border-color:var(--green3);color:var(--green)}
3505
+ .screenshot-preview{max-width:100%;border-radius:var(--r);margin:8px 0;border:1px solid var(--border);cursor:zoom-in;transition:opacity .15s}
3506
+ .screenshot-preview:hover{opacity:.88}
3507
+ .lightbox-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:9999;align-items:center;justify-content:center;cursor:zoom-out}
3508
+ .lightbox-overlay--open{display:flex}
3509
+ .lightbox-overlay img{max-width:92vw;max-height:88vh;border-radius:8px;object-fit:contain;box-shadow:0 8px 48px rgba(0,0,0,.8)}
3510
+ .lightbox-close{position:fixed;top:16px;right:20px;color:#fff;font-size:28px;cursor:pointer;line-height:1;opacity:.7}
3511
+ .lightbox-close:hover{opacity:1}
3512
+ .inline-card{margin:12px 0;padding:0;border-radius:10px;border:1px solid var(--border);overflow:hidden;background:var(--bg2)}
3513
+ .inline-card iframe{width:100%;height:280px;border:none;border-radius:0 0 10px 10px}
3514
+ .inline-card a{color:var(--cyan);text-decoration:none}
3515
+ .inline-card a:hover{text-decoration:underline}
3516
+ .inline-browser{margin:12px 0;border-radius:10px;border:1px solid var(--green3);overflow:hidden;background:#000}
3517
+ .inline-browser-bar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--bg);border-bottom:1px solid var(--border)}
3518
+ .inline-browser-dot{width:8px;height:8px;border-radius:50%;background:var(--border2)}
3519
+ .inline-browser-url{font-family:var(--mono);font-size:10px;color:var(--dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3520
+ .inline-browser img{width:100%;display:block}
3521
+ .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}
3522
+ .browser-viewer--open{display:block}
3523
+ .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}
3524
+ .browser-viewer__dot{width:6px;height:6px;border-radius:50%;background:#9c27b0;animation:bvpulse 1.5s infinite}
3525
+ @keyframes bvpulse{0%,100%{opacity:1}50%{opacity:.3}}
3526
+ .browser-viewer__title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3527
+ .browser-viewer__close{background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0 4px}
3528
+ .browser-viewer__close:hover{color:#fff}
3529
+ .browser-viewer__frame{width:100%;aspect-ratio:16/9;background:#000;display:flex;align-items:center;justify-content:center}
3530
+ .browser-viewer__frame img{width:100%;height:100%;object-fit:contain}
3531
+ .browser-viewer__status{padding:4px 10px;font-size:9px;color:var(--dim);border-top:1px solid var(--border)}
3532
+ @media(max-width:600px){.browser-viewer{width:calc(100vw - 24px);top:8px;left:8px}}
3533
+ @media(min-width:901px){.browser-viewer{left:232px}}
3534
+ .chat__bar{display:flex;flex-wrap:wrap;gap:6px;padding:10px 0 12px 0;border-top:1px solid var(--border);flex-shrink:0;align-items:flex-end}
3535
+ .chat__bar-tools{display:flex;gap:4px;align-items:center;width:100%}
3536
+ .chat__input{flex:1;resize:none;min-height:44px;max-height:120px;padding:10px 14px;font-size:14px}
3537
+ .chat__send{background:var(--green3);color:var(--bg);padding:10px 20px;border-radius:var(--r);font-weight:700;font-size:14px;white-space:nowrap}
3538
+ .chat__send:disabled{opacity:.4}
3539
+ .chat__stop{white-space:nowrap}
3540
+
3541
+ /* ---- MOBILE TOUCH (Termux / small screens) ---- */
3542
+ @media(max-width:600px){
3543
+ .msg--user .msg__bubble,.msg--assistant .msg__bubble{font-size:14px;padding:12px 14px;max-width:94%;line-height:1.55}
3544
+ .msg__label{font-size:11px}
3545
+ .chat__bar{flex-wrap:wrap;gap:6px;padding:8px 4px 10px 4px}
3546
+ .chat__bar-tools{flex-wrap:wrap;gap:4px}
3547
+ .chat__input{min-height:48px;font-size:15px;padding:12px 14px;width:100%;flex-basis:100%}
3548
+ .chat__send{padding:12px 20px;font-size:15px;flex-grow:1}
3549
+ .chat__stop{flex-grow:1}
3550
+ .chat__empty-title{font-size:22px}
3551
+ .header{padding:10px 12px}
3552
+ .header__title{font-size:15px}
3553
+ .content{padding:10px}
3554
+ /* Conversation sidebar as overlay on mobile, not side panel */
3555
+ #convSidebar{position:fixed!important;top:0!important;left:0!important;height:100dvh!important;width:260px!important;box-shadow:4px 0 20px rgba(0,0,0,0.8)!important;z-index:250!important}
3556
+ }
3557
+ .chat__stop{background:var(--red);color:var(--bright);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px;display:none}
3558
+ .chat__stop--visible{display:block}
3559
+
3560
+ /* ---- TASKS ---- */
3561
+ .task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
3562
+ .task-bar input{flex:1;min-width:150px}
3563
+ .task-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--r);font-size:12px}
3564
+ .task-bar button{background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
3565
+ .task{display:flex;align-items:center;gap:10px;padding:10px 14px}
3566
+ .task--done{opacity:.5}
3567
+ .task__check{width:18px;height:18px;border:2px solid var(--border2);border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:12px;color:var(--green)}
3568
+ .task__check--done{background:var(--green3);border-color:var(--green)}
3569
+ .task__desc{flex:1;min-width:0}
3570
+ .task__priority{font-size:9px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
3571
+ .task__priority--high,.task__priority--critical{background:var(--red);color:var(--bright)}
3572
+ .task__priority--medium{background:var(--amber);color:var(--bg)}
3573
+ .task__priority--low{background:var(--border2);color:var(--dim)}
3574
+
3575
+ /* ---- PLAN ---- */
3576
+ .plan-summary{padding:16px;border-left:3px solid var(--green);background:var(--bg2);border-radius:0 var(--r) var(--r) 0;margin-bottom:16px;line-height:1.6}
3577
+ .plan-action{display:flex;gap:10px;align-items:baseline;padding:6px 0}
3578
+ .plan-action__time{color:var(--amber);min-width:50px;font-weight:600}
3579
+ .plan-action__text{flex:1}
3580
+ .plan-action__priority{font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700}
3581
+
3582
+ /* ---- EMAILS ---- */
3583
+ .email{padding:12px 14px}
3584
+ .email__header{display:flex;justify-content:space-between;gap:8px}
3585
+ .email__from{color:var(--cyan);font-weight:600;font-size:12px}
3586
+ .email__date{color:var(--dim);font-size:10px;white-space:nowrap}
3587
+ .email__subject{color:var(--bright);margin-top:3px}
3588
+ .email__snippet{color:var(--dim);font-size:11px;margin-top:3px}
3589
+
3590
+ /* ---- CALENDAR ---- */
3591
+ .event{display:flex;gap:12px;align-items:center;padding:10px 14px}
3592
+ .event__time{color:var(--amber);font-weight:600;min-width:100px;white-space:nowrap;font-size:12px}
3593
+ .event__title{color:var(--bright);flex:1}
3594
+ .event__location{color:var(--dim);font-size:11px}
3595
+
3596
+ /* ---- AGENTS ---- */
3597
+ .agents-grid{display:grid;grid-template-columns:1fr;gap:6px}
3598
+ @media(min-width:500px){.agents-grid{grid-template-columns:1fr 1fr}}
3599
+ @media(min-width:901px){.agents-grid{grid-template-columns:1fr 1fr 1fr}}
3600
+ .agent-card{padding:8px 10px;text-align:left;cursor:pointer;transition:border-color .15s;display:flex;align-items:center;gap:10px}
3601
+ .agent-card:hover{border-color:var(--green3)}
3602
+ .agent-card__icon{font-size:18px;flex-shrink:0}
3603
+ .agent-card__body{min-width:0;flex:1}
3604
+ .agent-card__name{color:var(--green);font-weight:700;font-size:11px}
3605
+ .agent-card__tagline{font-size:9px;color:var(--text);line-height:1.3}
3606
+ .agent-card__cat{display:none}
3607
+
3608
+ /* ---- MODAL ---- */
3609
+ .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:300;align-items:center;justify-content:center}
3610
+ .modal-overlay--open{display:flex}
3611
+ .modal{background:var(--bg2);border:1px solid var(--border2);border-radius:8px;width:92%;max-width:560px;max-height:90vh;display:flex;flex-direction:column}
3612
+ .modal--chat{height:80vh}
3613
+ .modal__header{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--border)}
3614
+ .modal__header h2{font-size:16px;color:var(--green)}
3615
+ .modal__close{background:none;color:var(--dim);font-size:24px;padding:0 4px}
3616
+ .modal__body{padding:16px;overflow-y:auto;flex:1}
3617
+ .modal__body textarea{width:100%;min-height:80px;margin-bottom:10px}
3618
+ .modal__response{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:12px;word-wrap:break-word;max-height:400px;overflow-y:auto;font-size:13px}
3619
+ .modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--border)}
3620
+ .modal--chat .modal__body{padding:0;display:flex;flex-direction:column;overflow:hidden}
3621
+ .agent-chat{display:flex;flex-direction:column;height:100%;min-height:0;padding:12px 16px;box-sizing:border-box}
3622
+ .agent-chat__messages{flex:1;overflow-y:auto;padding:4px 0 8px;min-height:0;display:flex;flex-direction:column;gap:8px}
3623
+ .agent-chat__bubble{max-width:88%;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.55;word-wrap:break-word}
3624
+ .agent-chat__bubble--user{align-self:flex-end;background:var(--green3);color:#e8ffe8}
3625
+ .agent-chat__bubble--agent{align-self:flex-start;background:var(--bg3);border:1px solid var(--border)}
3626
+ .agent-chat__footer{display:flex;flex-direction:column;gap:6px;padding-top:8px;border-top:1px solid var(--border)}
3627
+ .agent-chat__drop{border:2px dashed var(--border2);border-radius:6px;padding:8px;text-align:center;color:var(--dim);font-size:11px;cursor:pointer;transition:border-color .2s}
3628
+ .agent-chat__drop:hover{border-color:var(--green)}
3629
+ .agent-chat__input-row{display:flex;gap:6px;align-items:flex-end}
3630
+ .agent-chat__input{flex:1;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:8px 10px;color:var(--fg);font-size:13px;font-family:var(--font);resize:none;min-height:36px;max-height:120px;overflow-y:auto;outline:none}
3631
+ .agent-chat__input:focus{border-color:var(--green)}
3632
+ .btn{padding:8px 16px;border-radius:var(--r);font-size:12px;font-weight:600}
3633
+ .btn--primary{background:var(--green3);color:var(--bg)}
3634
+ .btn--secondary{background:var(--bg3);color:var(--dim);border:1px solid var(--border)}
3635
+
3636
+ /* ---- SPINNER ---- */
3637
+ .spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin .6s linear infinite;margin:0 auto 12px}
3638
+ @keyframes spin{to{transform:rotate(360deg)}}
3639
+ .thinking-dots{display:inline-flex;gap:4px;align-items:center;padding:2px 0}
3640
+ .thinking-dots span{width:6px;height:6px;border-radius:50%;background:var(--dim);animation:tdot 1.2s ease-in-out infinite}
3641
+ .thinking-dots span:nth-child(2){animation-delay:.2s}
3642
+ .thinking-dots span:nth-child(3){animation-delay:.4s}
3643
+ @keyframes tdot{0%,80%,100%{opacity:.2;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}
3644
+
3645
+ /* ---- TOASTS (real-time notifications) ---- */
3646
+ .toast-container{position:fixed;top:16px;right:16px;z-index:500;display:flex;flex-direction:column;gap:8px;pointer-events:none}
3647
+ .toast{background:var(--bg3);border:1px solid var(--green);border-radius:8px;padding:12px 16px;max-width:320px;animation:toastIn .3s;pointer-events:auto;cursor:pointer;box-shadow:0 4px 20px rgba(0,0,0,0.5)}
3648
+ .toast--email{border-color:var(--cyan)}
3649
+ .toast--meeting{border-color:var(--amber)}
3650
+ .toast--security{border-color:var(--red)}
3651
+ .toast--plan{border-color:var(--green)}
3652
+ .toast__title{font-size:11px;font-weight:700;margin-bottom:3px}
3653
+ .toast--email .toast__title{color:var(--cyan)}
3654
+ .toast--meeting .toast__title{color:var(--amber)}
3655
+ .toast--security .toast__title{color:var(--red)}
3656
+ .toast--plan .toast__title{color:var(--green)}
3657
+ .toast__body{font-size:11px;color:var(--text);line-height:1.4}
3658
+ .toast--fadeout{animation:toastOut .3s forwards}
3659
+ @keyframes toastIn{from{transform:translateX(100%);opacity:0}to{transform:none;opacity:1}}
3660
+ @keyframes toastOut{from{opacity:1}to{opacity:0;transform:translateX(40px)}}
3661
+
3662
+ /* ---- VOICE MIC BUTTON (in chat bar) ---- */
3663
+ .chat__mic{background:var(--bg3);color:var(--green);border:1px solid var(--border2);width:40px;height:40px;border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer;flex-shrink:0;transition:all .2s}
3664
+ .chat__mic:hover{border-color:var(--green3);background:var(--greendim)}
3665
+ .chat__mic--recording{border-color:var(--red);color:var(--red);background:rgba(255,23,68,0.1);animation:micPulse 1.5s ease-in-out infinite}
3666
+ @keyframes micPulse{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 6px rgba(255,23,68,0.2)}}
3667
+
3668
+ /* ---- STUDIO ---- */
3669
+ .studio-header{margin-bottom:20px}
3670
+ .studio-header h2{font-size:15px;color:var(--green);margin-bottom:4px}
3671
+ .studio-header p{font-size:11px;color:var(--dim);line-height:1.5}
3672
+ .studio-input-row{display:flex;gap:8px;margin-bottom:20px}
3673
+ .studio-input-row textarea{flex:1;resize:none;height:60px;padding:10px 14px;font-size:13px;border-radius:var(--r);border:1px solid var(--border2)}
3674
+ .studio-input-row textarea:focus{border-color:var(--green3)}
3675
+ .studio-run-btn{background:var(--green3);color:var(--bg);padding:0 20px;border-radius:var(--r);font-weight:700;font-size:13px;white-space:nowrap;align-self:stretch;min-width:90px}
3676
+ .studio-run-btn:disabled{opacity:.4}
3677
+ .studio-canvas{position:relative;width:100%;min-height:220px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:20px;overflow:hidden}
3678
+ .studio-canvas__empty{display:flex;align-items:center;justify-content:center;height:180px;color:var(--dim);font-size:11px;flex-direction:column;gap:8px}
3679
+ .studio-canvas__empty-icon{font-size:32px;opacity:.3}
3680
+ .studio-nodes{display:flex;align-items:center;gap:0;padding:28px 24px;overflow-x:auto;min-height:130px;background:var(--bg2);border-radius:10px;border:1px solid var(--border);margin-bottom:16px}
3681
+ .studio-node{position:relative;display:flex;flex-direction:column;align-items:center;gap:7px;min-width:106px;max-width:126px}
3682
+ .studio-node__circle{width:56px;height:56px;border-radius:14px;border:1.5px solid var(--border2);background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:22px;transition:all .35s;flex-shrink:0}
3683
+ .studio-node--active .studio-node__circle{border-color:var(--green3);box-shadow:0 0 0 4px rgba(99,102,241,.15);background:var(--greendim);animation:stRing 1.4s ease-out infinite}
3684
+ .studio-node--done .studio-node__circle{border-color:#22c55e;background:rgba(34,197,94,.08);box-shadow:0 0 0 3px rgba(34,197,94,.12)}
3685
+ .studio-node--error .studio-node__circle{border-color:var(--red);background:rgba(239,68,68,.07)}
3686
+ .studio-node__label{font-size:10px;color:var(--dim);text-align:center;line-height:1.3;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
3687
+ .studio-node--active .studio-node__label{color:var(--green);font-weight:700}
3688
+ .studio-node--done .studio-node__label{color:#22c55e;font-weight:600}
3689
+ .studio-node__status{font-size:8px;padding:2px 8px;border-radius:20px;font-weight:700;text-transform:uppercase;letter-spacing:.6px}
3690
+ .studio-node__status--waiting{background:rgba(156,163,175,.1);color:var(--dim)}
3691
+ .studio-node__status--running{background:rgba(99,102,241,.15);color:var(--green);animation:stPulse .9s ease-in-out infinite}
3692
+ .studio-node__status--done{background:rgba(34,197,94,.12);color:#22c55e}
3693
+ .studio-node__status--error{background:rgba(239,68,68,.12);color:var(--red)}
3694
+ @keyframes stPulse{0%,100%{opacity:1}50%{opacity:.25}}
3695
+ @keyframes stRing{0%{box-shadow:0 0 0 4px rgba(99,102,241,.15)}70%{box-shadow:0 0 0 12px rgba(99,102,241,0)}100%{box-shadow:0 0 0 4px rgba(99,102,241,.15)}}
3696
+ @keyframes stFlow{0%{opacity:.4;transform:scaleX(.7)}100%{opacity:1;transform:scaleX(1)}}
3697
+ .studio-arrow{display:flex;align-items:center;color:var(--border2);font-size:18px;padding:0 8px;flex-shrink:0;margin-bottom:30px;transition:color .4s}
3698
+ .studio-arrow--active{color:var(--green3);animation:stFlow .5s ease-in-out infinite alternate}
3699
+ .studio-arrow--done{color:#22c55e}
3700
+ .studio-log{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px;max-height:380px;overflow-y:auto;font-size:11.5px;line-height:1.65}
3701
+ .studio-log-entry{margin-bottom:12px;padding:10px 12px;border-radius:8px;background:var(--bg3);border:1px solid var(--border)}
3702
+ .studio-log-entry:last-child{margin-bottom:0}
3703
+ .studio-log-entry__header{display:flex;align-items:center;gap:8px;margin-bottom:6px}
3704
+ .studio-log-entry__icon{font-size:15px}
3705
+ .studio-log-entry__agent{font-weight:700;color:var(--green);font-size:11px;letter-spacing:.3px}
3706
+ .studio-log-entry__time{color:var(--dim);font-size:9px;margin-left:auto;opacity:.7}
3707
+ .studio-log-entry__text{color:var(--text);word-wrap:break-word}
3708
+ .studio-log-entry--system .studio-log-entry__agent{color:var(--cyan)}
3709
+ .studio-log-entry--error .studio-log-entry__agent{color:var(--red)}
3710
+ .studio-result{margin-top:16px;padding:16px;background:var(--greendim);border:1px solid var(--green3);border-radius:8px}
3711
+ .studio-result__title{font-size:10px;color:var(--green);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
3712
+ .studio-result__body{font-size:13px;color:var(--text);word-wrap:break-word;line-height:1.7}
3713
+ .studio-example-btn{display:inline-block;padding:5px 12px;border:1px solid var(--border2);border-radius:20px;font-size:10px;color:var(--dim);cursor:pointer;background:none;margin:0 4px 6px 0;transition:all .15s}
3714
+ .studio-example-btn:hover{border-color:var(--green3);color:var(--green);background:var(--greendim)}
3715
+ .studio-tools-panel{width:220px;flex-shrink:0;border:1px solid var(--border);border-radius:10px;padding:12px;background:var(--bg2);max-height:600px;overflow-y:auto}
3716
+ .studio-tool-item{display:flex;align-items:flex-start;gap:8px;padding:8px;border-radius:6px;cursor:pointer;transition:background .15s;margin-bottom:4px}
3717
+ .studio-tool-item:hover{background:var(--bg3);border-radius:6px}
3718
+ .studio-tool-icon{font-size:16px;flex-shrink:0;margin-top:1px}
3719
+ .studio-session-item{padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;margin-bottom:8px}
3720
+ .studio-session-task{font-size:11px;color:var(--text);font-weight:500}
3721
+
3722
+ /* ---- MARKDOWN BODY ---- */
3723
+ .md-body{line-height:1.75}
3724
+ .md-body .md-p{margin:0 0 10px;color:var(--text);font-size:13.5px}
3725
+ .md-body .md-h1{font-size:18px;font-weight:700;color:var(--bright);margin:18px 0 10px;line-height:1.3}
3726
+ .md-body .md-h2{font-size:15px;font-weight:600;color:var(--bright);margin:16px 0 8px;line-height:1.3}
3727
+ .md-body .md-h3{font-size:13.5px;font-weight:600;color:var(--green);margin:12px 0 6px;line-height:1.3}
3728
+ .md-body .md-ul,.md-body .md-ol{margin:6px 0 12px 0;padding-left:22px}
3729
+ .md-body .md-ul li,.md-body .md-ol li{margin-bottom:5px;color:var(--text);font-size:13.5px;line-height:1.65}
3730
+ .md-body .md-ul{list-style:disc}
3731
+ .md-body .md-ol{list-style:decimal}
3732
+ .md-body .md-code{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:12px 14px;margin:10px 0;overflow-x:auto;font-family:var(--mono);font-size:12px;color:#a5b4fc;white-space:pre}
3733
+ .md-body .md-inline-code{background:var(--bg3);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:12px;color:#a5b4fc}
3734
+ .md-body .md-bq{border-left:3px solid var(--green3);padding:6px 12px;margin:8px 0;color:var(--dim);font-style:italic}
3735
+ .md-body .md-hr{border:none;border-top:1px solid var(--border);margin:14px 0}
3736
+ .md-body .md-spacer{height:6px}
3737
+ .md-body strong{font-weight:700;color:var(--bright)}
3738
+ .md-body em{font-style:italic;color:var(--dim)}
3739
+ .md-body del{text-decoration:line-through;color:var(--dim)}
3740
+ .md-body a{color:var(--cyan);text-decoration:underline}
3741
+
3742
+ /* ---- CHAT bubble markdown tweaks ---- */
3743
+ .msg--assistant .msg__bubble{white-space:normal}
3744
+ .msg--user .msg__bubble{white-space:pre-wrap}
2783
3745
  `;
2784
3746
 
2785
3747
  return `<!DOCTYPE html>
@@ -2793,6 +3755,9 @@ init();
2793
3755
  <meta name="apple-mobile-web-app-title" content="NHA">
2794
3756
  <meta name="theme-color" content="#0a0a0a">
2795
3757
  <link rel="manifest" href="/manifest.json">
3758
+ <link rel="preconnect" href="https://fonts.googleapis.com">
3759
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
3760
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
2796
3761
  <title>NHA</title>
2797
3762
  <style>${CSS}</style>
2798
3763
  </head>
@@ -2843,6 +3808,7 @@ init();
2843
3808
  <div class="sidebar__section">
2844
3809
  <div class="sidebar__label">AI</div>
2845
3810
  <div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">&#129302;</span> Agents</div>
3811
+ <div class="nav-item" data-view="studio" onclick="switchView('studio')"><span class="nav-item__icon">&#9881;</span> Studio <span style="font-size:8px;padding:1px 5px;border-radius:4px;background:rgba(99,102,241,.25);color:var(--green);margin-left:4px;font-weight:700">NEW</span></div>
2846
3812
  <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> AgentMessenger <span id="collabBadge" style="display:none;background:var(--red);color:#fff;font-size:9px;padding:1px 5px;border-radius:8px;margin-left:4px;font-family:var(--mono)">0</span></div>
2847
3813
  </div>
2848
3814
  <div class="sidebar__section">
@@ -2876,29 +3842,41 @@ init();
2876
3842
  </div>
2877
3843
 
2878
3844
  <div class="modal-overlay" id="agentModal">
2879
- <div class="modal">
3845
+ <div class="modal modal--chat">
2880
3846
  <div class="modal__header">
2881
- <h2 id="modalName">Agent</h2>
3847
+ <div><h2 id="modalName">Agent</h2><div id="modalAgentDesc" style="font-size:10px;color:var(--dim);margin-top:2px"></div></div>
2882
3848
  <button class="modal__close" onclick="closeModal()">&times;</button>
2883
3849
  </div>
2884
3850
  <div class="modal__body">
2885
- <textarea id="modalPrompt" placeholder="Ask this agent something..."></textarea>
2886
- <div id="fileDropZone" style="border:2px dashed var(--border2);border-radius:6px;padding:12px;text-align:center;color:var(--dim);font-size:11px;cursor:pointer;margin-bottom:10px;transition:border-color .2s" onclick="document.getElementById('fileInput').click()" ondragover="event.preventDefault();this.style.borderColor='var(--green)'" ondragleave="this.style.borderColor='var(--border2)'" ondrop="event.preventDefault();this.style.borderColor='var(--border2)';handleFileDrop(event)">
2887
- Drop a file here or click to attach
2888
- <input type="file" id="fileInput" style="display:none" onchange="handleFileSelect(this)">
3851
+ <div class="agent-chat">
3852
+ <div class="agent-chat__messages" id="agentMessages"></div>
3853
+ <div class="agent-chat__footer">
3854
+ <div class="agent-chat__drop" id="agentFileDropZone" onclick="document.getElementById('agentFileInput').click()" ondragover="event.preventDefault();this.style.borderColor='var(--green)'" ondragleave="this.style.borderColor=''" ondrop="event.preventDefault();this.style.borderColor='';handleFileDrop(event)">
3855
+ Drop a file or click to attach
3856
+ <input type="file" id="agentFileInput" style="display:none" onchange="handleFileSelect(this)">
3857
+ </div>
3858
+ <div id="agentFileInfo" style="display:none;font-size:10px;color:var(--cyan)"></div>
3859
+ <div class="agent-chat__input-row">
3860
+ <textarea class="agent-chat__input" id="modalPrompt" rows="1" placeholder="Ask this agent... (Enter to send)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){askAgent();event.preventDefault();}"></textarea>
3861
+ <button class="btn btn--primary" id="agentAskBtn" onclick="askAgent()" style="height:36px;padding:0 14px">Send</button>
3862
+ </div>
3863
+ </div>
2889
3864
  </div>
2890
- <div id="fileInfo" style="display:none;font-size:10px;color:var(--cyan);margin-bottom:8px"></div>
2891
- <div class="modal__response" id="modalResponse" style="display:none"></div>
2892
- </div>
2893
- <div class="modal__footer">
2894
- <button class="btn btn--secondary" onclick="closeModal()">Close</button>
2895
- <button class="btn btn--primary" onclick="askAgent()">Ask</button>
2896
3865
  </div>
2897
3866
  </div>
2898
3867
  </div>
2899
3868
 
2900
3869
  <div id="canvasPanel"><div class="cvs-header"><div style="display:flex;align-items:center;gap:8px"><button id="canvasTabC" onclick="canvasShowCanvas()" style="background:none;border:none;border-bottom:2px solid var(--green);color:var(--green);cursor:pointer;font-family:var(--mono);font-size:11px;padding:2px 6px">Canvas</button><button id="canvasTabB" onclick="canvasShowBrowser()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-family:var(--mono);font-size:11px;padding:2px 6px">Browser</button><span id="canvasTitle" style="font-family:var(--mono);color:var(--green);font-size:11px;margin-left:8px">Canvas</span></div><div style="display:flex;align-items:center;gap:4px"><span id="canvasNav" style="display:none;gap:4px"><button onclick="canvasPrev()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Previous">&#x25C0;</button><button onclick="canvasNext()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Next">&#x25B6;</button></span><button onclick="canvasCopyText()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:11px;font-family:var(--mono)" title="Copy text content">Copy</button><button onclick="canvasCopyHTML()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:11px;font-family:var(--mono)" title="Copy HTML source">HTML</button><button onclick="canvasCopyImage()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:11px;font-family:var(--mono)" title="Copy as image">IMG</button><button onclick="toggleCanvasSize()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Resize">&#x2922;</button><button onclick="closeCanvas()" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px" title="Close">&times;</button></div></div><iframe id="canvasFrame" sandbox="allow-scripts" srcdoc=""></iframe></div>
2901
- <script>${JS}</script>
3870
+ <div id="agentEditorOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:400;align-items:center;justify-content:center;padding:16px"></div>
3871
+ <div class="lightbox-overlay" id="lightboxOverlay" onclick="closeLightbox()">
3872
+ <span class="lightbox-close" onclick="closeLightbox()">&times;</span>
3873
+ <img id="lightboxImg" src="" alt="Preview">
3874
+ </div>
3875
+ <script src="/nha-ui.js?v=${ts}"></script>
2902
3876
  </body>
2903
3877
  </html>`;
2904
3878
  }
3879
+
3880
+ export function getJS() {
3881
+ return JS;
3882
+ }