nothumanallowed 3.1.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/ui.mjs +3 -1
- package/src/services/web-ui.mjs +409 -758
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops. Ask agents directly, plan your day with 5 specialist agents, manage tasks, connect Gmail + Calendar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -268,7 +268,9 @@ function sendJSON(res, statusCode, data) {
|
|
|
268
268
|
function sendHTML(res, html) {
|
|
269
269
|
res.writeHead(200, {
|
|
270
270
|
'Content-Type': 'text/html; charset=utf-8',
|
|
271
|
-
'Cache-Control': 'no-cache',
|
|
271
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
272
|
+
'Pragma': 'no-cache',
|
|
273
|
+
'Expires': '0',
|
|
272
274
|
});
|
|
273
275
|
res.end(html);
|
|
274
276
|
}
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -1,813 +1,464 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web UI —
|
|
3
|
-
* Terminal/hacker aesthetic matching NHA's green-on-black style.
|
|
4
|
-
* Zero dependencies. All CSS + JS inline.
|
|
2
|
+
* Web UI v2 — Rewritten from scratch. Mobile-first. BEM CSS. No escape hell.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
export function getHTML(port) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
13
|
-
<meta name="mobile-web-app-capable" content="yes">
|
|
14
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
15
|
-
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
16
|
-
<meta name="apple-mobile-web-app-title" content="NHA">
|
|
17
|
-
<meta name="theme-color" content="#0a0a0a">
|
|
18
|
-
<link rel="manifest" href="/manifest.json">
|
|
19
|
-
<title>NHA — Local Operations Console</title>
|
|
20
|
-
<style>
|
|
6
|
+
const ts = Date.now();
|
|
7
|
+
|
|
8
|
+
// CSS as a clean block — no template literal nesting issues
|
|
9
|
+
const CSS = `
|
|
21
10
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
22
11
|
:root{
|
|
23
12
|
--bg:#0a0a0a;--bg2:#111;--bg3:#1a1a1a;--bg4:#222;
|
|
24
13
|
--green:#00ff41;--green2:#00cc33;--green3:#00aa28;--greendim:#0a3a12;
|
|
25
|
-
--cyan:#00e5ff;--amber:#ffb300;--red:#ff1744
|
|
26
|
-
--text:#c8c8c8;--
|
|
27
|
-
--border:#1e1e1e;--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
14
|
+
--cyan:#00e5ff;--amber:#ffb300;--red:#ff1744;
|
|
15
|
+
--text:#c8c8c8;--dim:#666;--bright:#fff;
|
|
16
|
+
--border:#1e1e1e;--border2:#333;
|
|
17
|
+
--font:'JetBrains Mono','Fira Code','SF Mono','Consolas',monospace;
|
|
18
|
+
--r:6px;
|
|
30
19
|
}
|
|
31
|
-
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--
|
|
20
|
+
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;line-height:1.5}
|
|
32
21
|
a{color:var(--cyan);text-decoration:none}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
input,textarea{font-family:var(--mono);background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:var(--radius);outline:none;font-size:13px}
|
|
22
|
+
button{font-family:var(--font);cursor:pointer;border:none;outline:none}
|
|
23
|
+
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}
|
|
36
24
|
input:focus,textarea:focus{border-color:var(--green3)}
|
|
37
|
-
::-webkit-scrollbar{width:6px
|
|
25
|
+
::-webkit-scrollbar{width:6px}
|
|
38
26
|
::-webkit-scrollbar-track{background:var(--bg)}
|
|
39
27
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
40
|
-
::-webkit-scrollbar-thumb:hover{background:var(--borderbright)}
|
|
41
|
-
|
|
42
|
-
/* Layout */
|
|
43
|
-
#app{display:flex;height:100vh;width:100vw}
|
|
44
|
-
#sidebar{width:220px;min-width:220px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:0;overflow-y:auto}
|
|
45
|
-
#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
|
46
|
-
#header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--bg)}
|
|
47
|
-
#content{flex:1;overflow-y:auto;padding:20px}
|
|
48
|
-
|
|
49
|
-
/* Sidebar */
|
|
50
|
-
.sidebar-brand{padding:16px;border-bottom:1px solid var(--border)}
|
|
51
|
-
.sidebar-brand h1{font-size:16px;color:var(--green);font-weight:700;letter-spacing:2px}
|
|
52
|
-
.sidebar-brand p{font-size:10px;color:var(--textdim);margin-top:2px}
|
|
53
|
-
.nav-section{padding:12px 0}
|
|
54
|
-
.nav-section-label{padding:0 16px;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--textdim);margin-bottom:4px}
|
|
55
|
-
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;color:var(--textdim);cursor:pointer;transition:all .15s;font-size:12px;border-left:2px solid transparent}
|
|
56
|
-
.nav-item:hover{color:var(--text);background:var(--bg3)}
|
|
57
|
-
.nav-item.active{color:var(--green);background:var(--greendim);border-left-color:var(--green)}
|
|
58
|
-
.nav-item .icon{width:16px;text-align:center;font-size:14px}
|
|
59
|
-
.nav-item .badge{margin-left:auto;background:var(--green3);color:var(--bg);padding:1px 6px;border-radius:8px;font-size:10px;font-weight:700}
|
|
60
|
-
|
|
61
|
-
/* Cards */
|
|
62
|
-
.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
|
|
63
|
-
.card-title{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--textdim);margin-bottom:8px}
|
|
64
|
-
.card-value{font-size:24px;font-weight:700;color:var(--green)}
|
|
65
|
-
.card-sub{font-size:11px;color:var(--textdim);margin-top:4px}
|
|
66
|
-
|
|
67
|
-
/* Dashboard grid */
|
|
68
|
-
.dash-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:20px}
|
|
69
|
-
.dash-section{margin-bottom:24px}
|
|
70
|
-
.dash-section h2{font-size:13px;color:var(--cyan);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px}
|
|
71
|
-
|
|
72
|
-
/* Chat */
|
|
73
|
-
#chat-view{display:flex;flex-direction:column;height:calc(100vh - 52px);overflow:hidden}
|
|
74
|
-
#chat-messages{flex:1;overflow-y:auto;padding:0 0 12px 0;-webkit-overflow-scrolling:touch}
|
|
75
|
-
.msg{margin-bottom:12px;display:flex;gap:10px}
|
|
76
|
-
.msg-user .msg-bubble{background:var(--bg3);border:1px solid var(--borderbright);border-radius:8px 8px 2px 8px;padding:10px 14px;max-width:80%;margin-left:auto;color:var(--textbright)}
|
|
77
|
-
.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}
|
|
78
|
-
.msg-label{font-size:10px;color:var(--textdim);margin-bottom:2px}
|
|
79
|
-
.msg-thinking{color:var(--textdim);font-style:italic;animation:pulse 1.5s infinite}
|
|
80
|
-
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
81
|
-
#chat-input-bar{display:flex;gap:8px;padding:12px 0 0 0;border-top:1px solid var(--border)}
|
|
82
|
-
#chat-input{flex:1;resize:none;min-height:40px;max-height:120px;padding:10px 14px}
|
|
83
|
-
#chat-send{background:var(--green3);color:var(--bg);padding:10px 20px;border-radius:var(--radius);font-weight:700;font-size:12px;transition:background .15s}
|
|
84
|
-
#chat-send:hover{background:var(--green2)}
|
|
85
|
-
#chat-send:disabled{opacity:.4;cursor:not-allowed}
|
|
86
|
-
|
|
87
|
-
/* Tasks */
|
|
88
|
-
.task-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);transition:background .1s}
|
|
89
|
-
.task-item:hover{background:var(--bg3)}
|
|
90
|
-
.task-check{width:18px;height:18px;border:2px solid var(--borderbright);border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
|
|
91
|
-
.task-check:hover{border-color:var(--green)}
|
|
92
|
-
.task-check.done{background:var(--green3);border-color:var(--green)}
|
|
93
|
-
.task-check.done::after{content:'\\2713';color:var(--bg);font-size:12px;font-weight:700}
|
|
94
|
-
.task-desc{flex:1;min-width:0}
|
|
95
|
-
.task-desc.done{text-decoration:line-through;color:var(--textdim)}
|
|
96
|
-
.task-priority{font-size:10px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
|
|
97
|
-
.task-priority.critical{background:var(--red);color:#fff}
|
|
98
|
-
.task-priority.high{background:var(--amber);color:#000}
|
|
99
|
-
.task-priority.medium{background:var(--bg4);color:var(--text)}
|
|
100
|
-
.task-priority.low{background:var(--bg3);color:var(--textdim)}
|
|
101
|
-
.task-add-bar{display:flex;gap:8px;margin-bottom:16px}
|
|
102
|
-
.task-add-bar input{flex:1}
|
|
103
|
-
.task-add-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--radius);font-size:12px}
|
|
104
|
-
.task-add-bar button{background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--radius);font-weight:700;font-size:12px}
|
|
105
|
-
|
|
106
|
-
/* Emails */
|
|
107
|
-
.email-item{padding:12px;border-bottom:1px solid var(--border);cursor:default}
|
|
108
|
-
.email-item:hover{background:var(--bg3)}
|
|
109
|
-
.email-from{color:var(--cyan);font-size:12px;font-weight:600}
|
|
110
|
-
.email-subject{color:var(--textbright);margin-top:2px}
|
|
111
|
-
.email-snippet{color:var(--textdim);font-size:11px;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
112
|
-
.email-date{color:var(--textdim);font-size:10px;margin-top:4px}
|
|
113
|
-
|
|
114
|
-
/* Calendar */
|
|
115
|
-
.event-item{display:flex;gap:12px;padding:12px;border-bottom:1px solid var(--border)}
|
|
116
|
-
.event-time{color:var(--amber);font-weight:600;min-width:110px;white-space:nowrap}
|
|
117
|
-
.event-title{color:var(--textbright)}
|
|
118
|
-
.event-location{color:var(--textdim);font-size:11px;margin-top:2px}
|
|
119
|
-
.event-cal{color:var(--textdim);font-size:10px}
|
|
120
|
-
|
|
121
|
-
/* Plan */
|
|
122
|
-
.plan-section{margin-bottom:20px}
|
|
123
|
-
.plan-section h3{font-size:12px;color:var(--amber);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border)}
|
|
124
|
-
.plan-summary{color:var(--textbright);font-size:14px;line-height:1.7;padding:12px 0}
|
|
125
|
-
.plan-schedule-item{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)}
|
|
126
|
-
.plan-schedule-time{color:var(--cyan);min-width:100px;font-weight:600}
|
|
127
|
-
.plan-schedule-type{font-size:10px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700;min-width:60px;text-align:center}
|
|
128
|
-
.plan-schedule-type.meeting{background:#003d5c;color:var(--cyan)}
|
|
129
|
-
.plan-schedule-type.focus{background:var(--greendim);color:var(--green)}
|
|
130
|
-
.plan-schedule-type.break{background:var(--bg4);color:var(--textdim)}
|
|
131
|
-
.plan-schedule-type.task{background:#3d2e00;color:var(--amber)}
|
|
132
|
-
.plan-action-item{padding:6px 0;display:flex;gap:8px;align-items:baseline}
|
|
133
|
-
.plan-action-badge{font-size:10px;padding:1px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
|
|
134
|
-
.plan-action-badge.critical{background:var(--red);color:#fff}
|
|
135
|
-
.plan-action-badge.high{background:var(--amber);color:#000}
|
|
136
|
-
.plan-action-badge.medium,.plan-action-badge.low{background:var(--bg4);color:var(--text)}
|
|
137
|
-
.plan-alert{padding:8px 12px;background:#2a0000;border:1px solid #5a0000;border-radius:var(--radius);margin-bottom:6px;color:var(--red)}
|
|
138
|
-
.plan-insight{color:var(--textdim);padding:4px 0}
|
|
139
|
-
.plan-meta{color:var(--textdim);font-size:11px;margin-top:12px;padding-top:8px;border-top:1px solid var(--border)}
|
|
140
|
-
.refresh-btn{background:var(--bg3);color:var(--cyan);padding:6px 14px;border-radius:var(--radius);font-size:11px;border:1px solid var(--border);transition:all .15s}
|
|
141
|
-
.refresh-btn:hover{background:var(--bg4);border-color:var(--cyan)}
|
|
142
|
-
|
|
143
|
-
/* Agents */
|
|
144
|
-
.agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
|
|
145
|
-
.agent-card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:14px;cursor:pointer;transition:all .15s}
|
|
146
|
-
.agent-card:hover{border-color:var(--green3);background:var(--bg3)}
|
|
147
|
-
.agent-name{font-size:13px;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:1px}
|
|
148
|
-
.agent-category{font-size:10px;color:var(--textdim);margin-top:2px}
|
|
149
|
-
.agent-tagline{font-size:11px;color:var(--text);margin-top:6px;line-height:1.4}
|
|
150
|
-
|
|
151
|
-
/* Modal */
|
|
152
|
-
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
153
|
-
.modal-overlay.show{opacity:1;pointer-events:all}
|
|
154
|
-
.modal{background:var(--bg2);border:1px solid var(--borderbright);border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;overflow:hidden}
|
|
155
|
-
.modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--border)}
|
|
156
|
-
.modal-header h2{font-size:14px;color:var(--green)}
|
|
157
|
-
.modal-close{background:none;color:var(--textdim);font-size:20px;padding:4px 8px;border-radius:4px}
|
|
158
|
-
.modal-close:hover{color:var(--text);background:var(--bg3)}
|
|
159
|
-
.modal-body{padding:16px;overflow-y:auto;flex:1}
|
|
160
|
-
.modal-body textarea{width:100%;min-height:80px;margin-bottom:12px}
|
|
161
|
-
.modal-body .response{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius);padding:12px;white-space:pre-wrap;word-wrap:break-word;max-height:300px;overflow-y:auto;color:var(--text);line-height:1.6}
|
|
162
|
-
.modal-footer{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
|
163
|
-
.modal-footer button{padding:8px 20px;border-radius:var(--radius);font-size:12px;font-weight:600}
|
|
164
|
-
.btn-primary{background:var(--green3);color:var(--bg)}
|
|
165
|
-
.btn-primary:hover{background:var(--green2)}
|
|
166
|
-
.btn-secondary{background:var(--bg3);color:var(--text);border:1px solid var(--border)}
|
|
167
|
-
.btn-secondary:hover{background:var(--bg4)}
|
|
168
|
-
|
|
169
|
-
/* Status indicator */
|
|
170
|
-
.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
|
|
171
|
-
.status-dot.ok{background:var(--green)}
|
|
172
|
-
.status-dot.warn{background:var(--amber)}
|
|
173
|
-
.status-dot.err{background:var(--red)}
|
|
174
|
-
|
|
175
|
-
/* Loading */
|
|
176
|
-
.loading{text-align:center;padding:40px;color:var(--textdim)}
|
|
177
|
-
.spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin .6s linear infinite;margin-bottom:8px}
|
|
178
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
179
|
-
|
|
180
|
-
/* Header */
|
|
181
|
-
.header-title{font-size:14px;color:var(--textbright);font-weight:600}
|
|
182
|
-
.header-time{margin-left:auto;color:var(--textdim);font-size:12px}
|
|
183
|
-
.header-status{font-size:11px}
|
|
184
|
-
|
|
185
|
-
/* Mobile */
|
|
186
|
-
#mobile-toggle{display:none;background:none;color:var(--green);font-size:20px;padding:4px 8px}
|
|
187
|
-
@media(max-width:768px){
|
|
188
|
-
#sidebar{position:fixed;left:-260px;top:0;height:100%;z-index:100;transition:left .25s;width:260px;box-shadow:4px 0 20px rgba(0,0,0,0.8)}
|
|
189
|
-
#sidebar.open{left:0}
|
|
190
|
-
#sidebar.open~#main #content::before{content:'';position:fixed;inset:52px 0 0 0;background:rgba(0,0,0,0.6);z-index:50}
|
|
191
|
-
#mobile-toggle{display:block}
|
|
192
|
-
#content{padding:12px}
|
|
193
|
-
.dash-grid{grid-template-columns:1fr}
|
|
194
|
-
.agents-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
|
195
|
-
.msg-user .msg-bubble,.msg-assistant .msg-bubble{max-width:95%}
|
|
196
|
-
.modal{width:95%;max-width:none}
|
|
197
|
-
}
|
|
198
|
-
</style>
|
|
199
|
-
</head>
|
|
200
|
-
<body>
|
|
201
|
-
<div id="app">
|
|
202
|
-
<nav id="sidebar">
|
|
203
|
-
<div class="sidebar-brand">
|
|
204
|
-
<h1>NHA</h1>
|
|
205
|
-
<p>Local Ops Console</p>
|
|
206
|
-
</div>
|
|
207
|
-
<div class="nav-section">
|
|
208
|
-
<div class="nav-section-label">Operations</div>
|
|
209
|
-
<div class="nav-item active" data-view="dashboard" onclick="switchView('dashboard')">
|
|
210
|
-
<span class="icon">▣</span> Dashboard
|
|
211
|
-
</div>
|
|
212
|
-
<div class="nav-item" data-view="chat" onclick="switchView('chat')">
|
|
213
|
-
<span class="icon">▶</span> Chat
|
|
214
|
-
</div>
|
|
215
|
-
<div class="nav-item" data-view="plan" onclick="switchView('plan')">
|
|
216
|
-
<span class="icon">☰</span> Plan
|
|
217
|
-
</div>
|
|
218
|
-
<div class="nav-item" data-view="tasks" onclick="switchView('tasks')">
|
|
219
|
-
<span class="icon">☑</span> Tasks <span class="badge" id="task-badge" style="display:none">0</span>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
<div class="nav-section">
|
|
223
|
-
<div class="nav-section-label">Data</div>
|
|
224
|
-
<div class="nav-item" data-view="emails" onclick="switchView('emails')">
|
|
225
|
-
<span class="icon">✉</span> Emails <span class="badge" id="email-badge" style="display:none">0</span>
|
|
226
|
-
</div>
|
|
227
|
-
<div class="nav-item" data-view="calendar" onclick="switchView('calendar')">
|
|
228
|
-
<span class="icon">📅</span> Calendar
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
<div class="nav-section">
|
|
232
|
-
<div class="nav-section-label">Intelligence</div>
|
|
233
|
-
<div class="nav-item" data-view="agents" onclick="switchView('agents')">
|
|
234
|
-
<span class="icon">★</span> Agents <span class="badge">38</span>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
</nav>
|
|
238
|
-
|
|
239
|
-
<div id="main">
|
|
240
|
-
<div id="header">
|
|
241
|
-
<button id="mobile-toggle" onclick="toggleSidebar()">☰</button>
|
|
242
|
-
<span class="header-title" id="header-title">Dashboard</span>
|
|
243
|
-
<span class="header-status" id="header-status"></span>
|
|
244
|
-
<span class="header-time" id="header-time"></span>
|
|
245
|
-
</div>
|
|
246
|
-
<div id="content"></div>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<!-- Agent Modal -->
|
|
251
|
-
<div class="modal-overlay" id="agent-modal">
|
|
252
|
-
<div class="modal">
|
|
253
|
-
<div class="modal-header">
|
|
254
|
-
<h2 id="modal-agent-name">AGENT</h2>
|
|
255
|
-
<button class="modal-close" onclick="closeModal()">×</button>
|
|
256
|
-
</div>
|
|
257
|
-
<div class="modal-body">
|
|
258
|
-
<textarea id="modal-prompt" placeholder="Ask this agent something..."></textarea>
|
|
259
|
-
<div id="modal-response" class="response" style="display:none"></div>
|
|
260
|
-
</div>
|
|
261
|
-
<div class="modal-footer">
|
|
262
|
-
<button class="btn-secondary" onclick="closeModal()">Close</button>
|
|
263
|
-
<button class="btn-primary" id="modal-send" onclick="askAgent()">Ask</button>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
28
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
29
|
+
/* ---- LAYOUT: mobile-first ---- */
|
|
30
|
+
.app{display:flex;flex-direction:column;height:100vh;height:100dvh}
|
|
31
|
+
.header{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border);background:var(--bg);position:relative;z-index:60;flex-shrink:0}
|
|
32
|
+
.header__burger{background:none;color:var(--green);font-size:22px;padding:4px 8px;line-height:1}
|
|
33
|
+
.header__title{font-size:14px;color:var(--bright);font-weight:700;flex:1}
|
|
34
|
+
.header__clock{font-size:10px;color:var(--dim)}
|
|
35
|
+
|
|
36
|
+
.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)}
|
|
37
|
+
.sidebar--open{display:flex}
|
|
38
|
+
.sidebar__overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:150}
|
|
39
|
+
.sidebar__overlay--open{display:block}
|
|
40
|
+
.sidebar__brand{padding:16px;border-bottom:1px solid var(--border)}
|
|
41
|
+
.sidebar__brand-name{font-size:16px;color:var(--green);font-weight:700;letter-spacing:2px}
|
|
42
|
+
.sidebar__brand-sub{font-size:10px;color:var(--dim);margin-top:2px}
|
|
43
|
+
.sidebar__section{padding:12px 0}
|
|
44
|
+
.sidebar__label{padding:0 16px;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--dim);margin-bottom:4px}
|
|
45
|
+
.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}
|
|
46
|
+
.nav-item:hover,.nav-item--active{color:var(--bright);background:var(--bg3)}
|
|
47
|
+
.nav-item--active{border-left-color:var(--green);color:var(--green)}
|
|
48
|
+
.nav-item__icon{width:18px;text-align:center}
|
|
49
|
+
.nav-item__badge{background:var(--red);color:var(--bright);font-size:9px;padding:1px 5px;border-radius:8px;margin-left:auto}
|
|
50
|
+
|
|
51
|
+
.content{flex:1;overflow-y:auto;padding:16px;-webkit-overflow-scrolling:touch}
|
|
52
|
+
|
|
53
|
+
/* ---- DESKTOP: sidebar always visible ---- */
|
|
54
|
+
@media(min-width:901px){
|
|
55
|
+
.app{flex-direction:row}
|
|
56
|
+
.header__burger{display:none}
|
|
57
|
+
.sidebar{display:flex!important;position:static;width:220px;min-width:220px;height:auto;box-shadow:none}
|
|
58
|
+
.sidebar__overlay{display:none!important}
|
|
59
|
+
.content{padding:20px}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---- CARDS ---- */
|
|
63
|
+
.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:10px}
|
|
64
|
+
.card__title{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin-bottom:6px}
|
|
65
|
+
.card__value{font-size:22px;font-weight:700;color:var(--green)}
|
|
66
|
+
.card__sub{font-size:11px;color:var(--dim);margin-top:3px}
|
|
67
|
+
.dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
|
|
68
|
+
@media(min-width:901px){.dash-grid{grid-template-columns:repeat(4,1fr)}}
|
|
69
|
+
.section-title{font-size:12px;color:var(--cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
|
|
70
|
+
|
|
71
|
+
/* ---- CHAT ---- */
|
|
72
|
+
.chat{display:flex;flex-direction:column;height:calc(100vh - 52px);height:calc(100dvh - 52px)}
|
|
73
|
+
@media(min-width:901px){.chat{height:calc(100vh - 52px)}}
|
|
74
|
+
.chat__messages{flex:1;overflow-y:auto;padding-bottom:12px;-webkit-overflow-scrolling:touch}
|
|
75
|
+
.chat__empty{text-align:center;padding:60px 16px;color:var(--dim)}
|
|
76
|
+
.chat__empty-title{font-size:28px;color:var(--green);margin-bottom:12px}
|
|
77
|
+
.chat__empty-hint{font-size:11px;margin-top:12px}
|
|
78
|
+
.msg{margin-bottom:12px}
|
|
79
|
+
.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)}
|
|
80
|
+
.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}
|
|
81
|
+
.msg__label{font-size:10px;color:var(--dim);margin-bottom:2px}
|
|
82
|
+
.msg--thinking{color:var(--dim);font-style:italic}
|
|
83
|
+
.chat__bar{display:flex;gap:8px;padding:10px 0 0 0;border-top:1px solid var(--border);flex-shrink:0}
|
|
84
|
+
.chat__input{flex:1;resize:none;min-height:40px;max-height:100px;padding:10px 14px}
|
|
85
|
+
.chat__send{background:var(--green3);color:var(--bg);padding:10px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
|
|
86
|
+
.chat__send:disabled{opacity:.4}
|
|
87
|
+
|
|
88
|
+
/* ---- TASKS ---- */
|
|
89
|
+
.task-bar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
|
90
|
+
.task-bar input{flex:1;min-width:150px}
|
|
91
|
+
.task-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--r);font-size:12px}
|
|
92
|
+
.task-bar button{background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px}
|
|
93
|
+
.task{display:flex;align-items:center;gap:10px;padding:10px 14px}
|
|
94
|
+
.task--done{opacity:.5}
|
|
95
|
+
.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)}
|
|
96
|
+
.task__check--done{background:var(--green3);border-color:var(--green)}
|
|
97
|
+
.task__desc{flex:1;min-width:0}
|
|
98
|
+
.task__priority{font-size:9px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
|
|
99
|
+
.task__priority--high,.task__priority--critical{background:var(--red);color:var(--bright)}
|
|
100
|
+
.task__priority--medium{background:var(--amber);color:var(--bg)}
|
|
101
|
+
.task__priority--low{background:var(--border2);color:var(--dim)}
|
|
102
|
+
|
|
103
|
+
/* ---- PLAN ---- */
|
|
104
|
+
.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}
|
|
105
|
+
.plan-action{display:flex;gap:10px;align-items:baseline;padding:6px 0}
|
|
106
|
+
.plan-action__time{color:var(--amber);min-width:50px;font-weight:600}
|
|
107
|
+
.plan-action__text{flex:1}
|
|
108
|
+
.plan-action__priority{font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700}
|
|
109
|
+
|
|
110
|
+
/* ---- EMAILS ---- */
|
|
111
|
+
.email{padding:12px 14px}
|
|
112
|
+
.email__header{display:flex;justify-content:space-between;gap:8px}
|
|
113
|
+
.email__from{color:var(--cyan);font-weight:600;font-size:12px}
|
|
114
|
+
.email__date{color:var(--dim);font-size:10px;white-space:nowrap}
|
|
115
|
+
.email__subject{color:var(--bright);margin-top:3px}
|
|
116
|
+
.email__snippet{color:var(--dim);font-size:11px;margin-top:3px}
|
|
117
|
+
|
|
118
|
+
/* ---- CALENDAR ---- */
|
|
119
|
+
.event{display:flex;gap:12px;align-items:center;padding:10px 14px}
|
|
120
|
+
.event__time{color:var(--amber);font-weight:600;min-width:100px;white-space:nowrap;font-size:12px}
|
|
121
|
+
.event__title{color:var(--bright);flex:1}
|
|
122
|
+
.event__location{color:var(--dim);font-size:11px}
|
|
123
|
+
|
|
124
|
+
/* ---- AGENTS ---- */
|
|
125
|
+
.agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px}
|
|
126
|
+
@media(min-width:901px){.agents-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}}
|
|
127
|
+
.agent-card{padding:12px;text-align:center;cursor:pointer;transition:border-color .15s}
|
|
128
|
+
.agent-card:hover{border-color:var(--green3)}
|
|
129
|
+
.agent-card__name{color:var(--green);font-weight:700;font-size:13px;margin-bottom:4px}
|
|
130
|
+
.agent-card__cat{font-size:9px;color:var(--dim);text-transform:uppercase}
|
|
131
|
+
|
|
132
|
+
/* ---- MODAL ---- */
|
|
133
|
+
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:300;align-items:center;justify-content:center}
|
|
134
|
+
.modal-overlay--open{display:flex}
|
|
135
|
+
.modal{background:var(--bg2);border:1px solid var(--border2);border-radius:8px;width:90%;max-width:500px;max-height:80vh;display:flex;flex-direction:column}
|
|
136
|
+
.modal__header{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--border)}
|
|
137
|
+
.modal__header h2{font-size:16px;color:var(--green)}
|
|
138
|
+
.modal__close{background:none;color:var(--dim);font-size:24px;padding:0 4px}
|
|
139
|
+
.modal__body{padding:16px;overflow-y:auto;flex:1}
|
|
140
|
+
.modal__body textarea{width:100%;min-height:80px;margin-bottom:10px}
|
|
141
|
+
.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}
|
|
142
|
+
.modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--border)}
|
|
143
|
+
.btn{padding:8px 16px;border-radius:var(--r);font-size:12px;font-weight:600}
|
|
144
|
+
.btn--primary{background:var(--green3);color:var(--bg)}
|
|
145
|
+
.btn--secondary{background:var(--bg3);color:var(--dim);border:1px solid var(--border)}
|
|
146
|
+
|
|
147
|
+
/* ---- SPINNER ---- */
|
|
148
|
+
.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}
|
|
149
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
// JS as clean block
|
|
153
|
+
const JS = `
|
|
154
|
+
var API = '';
|
|
155
|
+
var currentView = 'dashboard';
|
|
156
|
+
var chatHistory = [];
|
|
157
|
+
var dash = {emails:[],events:[],tasks:[],plan:null,status:null};
|
|
158
|
+
var agentsList = [];
|
|
159
|
+
var selectedAgent = null;
|
|
160
|
+
|
|
161
|
+
// ---- NAV ----
|
|
162
|
+
function switchView(v) {
|
|
163
|
+
currentView = v;
|
|
164
|
+
document.querySelectorAll('.nav-item').forEach(function(el){
|
|
165
|
+
if(el.dataset.view===v){el.classList.add('nav-item--active')}else{el.classList.remove('nav-item--active')}
|
|
282
166
|
});
|
|
283
|
-
|
|
284
|
-
document.getElementById('
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function toggleSidebar() {
|
|
290
|
-
document.getElementById('sidebar').classList.toggle('open');
|
|
167
|
+
var titles = {dashboard:'Dashboard',chat:'Chat',plan:'Daily Plan',tasks:'Tasks',emails:'Emails',calendar:'Calendar',agents:'Agents'};
|
|
168
|
+
document.getElementById('headerTitle').textContent = titles[v]||v;
|
|
169
|
+
closeSidebar();
|
|
170
|
+
render();
|
|
291
171
|
}
|
|
292
|
-
|
|
293
|
-
document.getElementById('
|
|
294
|
-
|
|
295
|
-
if (sidebar.classList.contains('open')) {
|
|
296
|
-
sidebar.classList.remove('open');
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ── Clock ──────────────────────────────────────────────────────────────────
|
|
301
|
-
function updateClock() {
|
|
302
|
-
const now = new Date();
|
|
303
|
-
document.getElementById('header-time').textContent = now.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false}) + ' ' + now.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'});
|
|
304
|
-
}
|
|
305
|
-
setInterval(updateClock, 1000);
|
|
306
|
-
updateClock();
|
|
307
|
-
|
|
308
|
-
// ── API Helpers ────────────────────────────────────────────────────────────
|
|
309
|
-
async function apiGet(path) {
|
|
310
|
-
try {
|
|
311
|
-
const r = await fetch(API + path);
|
|
312
|
-
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
|
313
|
-
return await r.json();
|
|
314
|
-
} catch (e) {
|
|
315
|
-
console.error('API error:', path, e);
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
172
|
+
function openSidebar() {
|
|
173
|
+
document.getElementById('sidebar').classList.add('sidebar--open');
|
|
174
|
+
document.getElementById('overlay').classList.add('sidebar__overlay--open');
|
|
318
175
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const r = await fetch(API + path, {
|
|
323
|
-
method: 'POST',
|
|
324
|
-
headers: {'Content-Type': 'application/json'},
|
|
325
|
-
body: JSON.stringify(body),
|
|
326
|
-
});
|
|
327
|
-
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
|
328
|
-
return await r.json();
|
|
329
|
-
} catch (e) {
|
|
330
|
-
console.error('API error:', path, e);
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function apiPatch(path) {
|
|
336
|
-
try {
|
|
337
|
-
const r = await fetch(API + path, {method: 'PATCH'});
|
|
338
|
-
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
|
339
|
-
return await r.json();
|
|
340
|
-
} catch (e) {
|
|
341
|
-
console.error('API error:', path, e);
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
176
|
+
function closeSidebar() {
|
|
177
|
+
document.getElementById('sidebar').classList.remove('sidebar--open');
|
|
178
|
+
document.getElementById('overlay').classList.remove('sidebar__overlay--open');
|
|
344
179
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const [status, emails, events, tasks] = await Promise.all([
|
|
349
|
-
apiGet('/api/status'),
|
|
350
|
-
apiGet('/api/emails'),
|
|
351
|
-
apiGet('/api/calendar'),
|
|
352
|
-
apiGet('/api/tasks'),
|
|
353
|
-
]);
|
|
354
|
-
dashData.status = status;
|
|
355
|
-
dashData.emails = emails?.emails || [];
|
|
356
|
-
dashData.events = events?.events || [];
|
|
357
|
-
dashData.tasks = tasks?.tasks || [];
|
|
358
|
-
updateBadges();
|
|
180
|
+
function toggleSidebar() {
|
|
181
|
+
var sb = document.getElementById('sidebar');
|
|
182
|
+
if(sb.classList.contains('sidebar--open')){closeSidebar()}else{openSidebar()}
|
|
359
183
|
}
|
|
360
184
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
185
|
+
// ---- CLOCK ----
|
|
186
|
+
function updateClock(){
|
|
187
|
+
var d=new Date();
|
|
188
|
+
var el=document.getElementById('clock');
|
|
189
|
+
if(el)el.textContent=d.toLocaleTimeString('en',{hour:'2-digit',minute:'2-digit',hour12:false})+' '+d.toLocaleDateString('en',{weekday:'short',month:'short',day:'numeric'});
|
|
365
190
|
}
|
|
191
|
+
setInterval(updateClock,1000);updateClock();
|
|
366
192
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
193
|
+
// ---- API ----
|
|
194
|
+
function apiGet(p){return fetch(API+p).then(function(r){return r.ok?r.json():null}).catch(function(){return null})}
|
|
195
|
+
function apiPost(p,b){return fetch(API+p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)}).then(function(r){return r.ok?r.json():null}).catch(function(){return null})}
|
|
196
|
+
function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){return r.ok?r.json():null}).catch(function(){return null})}
|
|
371
197
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} else {
|
|
379
|
-
eb.style.display = 'none';
|
|
380
|
-
}
|
|
381
|
-
const pending = dashData.tasks.filter(t => t.status !== 'done').length;
|
|
382
|
-
if (pending > 0) {
|
|
383
|
-
tb.textContent = pending;
|
|
384
|
-
tb.style.display = '';
|
|
385
|
-
} else {
|
|
386
|
-
tb.style.display = 'none';
|
|
387
|
-
}
|
|
198
|
+
// ---- LOAD DATA ----
|
|
199
|
+
function loadDash(){
|
|
200
|
+
return Promise.all([apiGet('/api/status'),apiGet('/api/emails'),apiGet('/api/calendar'),apiGet('/api/tasks')]).then(function(r){
|
|
201
|
+
dash.status=r[0];dash.emails=(r[1]&&r[1].emails)||[];dash.events=(r[2]&&r[2].events)||[];dash.tasks=(r[3]&&r[3].tasks)||[];
|
|
202
|
+
updateBadges();
|
|
203
|
+
});
|
|
388
204
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
205
|
+
function loadAgents(){return apiGet('/api/agents').then(function(r){agentsList=(r&&r.agents)||[]})}
|
|
206
|
+
function updateBadges(){
|
|
207
|
+
var eb=document.getElementById('emailBadge'),tb=document.getElementById('taskBadge');
|
|
208
|
+
var ue=dash.emails.length,ut=dash.tasks.filter(function(t){return t.status!=='done'}).length;
|
|
209
|
+
if(eb){eb.textContent=ue;eb.style.display=ue>0?'':'none'}
|
|
210
|
+
if(tb){tb.textContent=ut;tb.style.display=ut>0?'':'none'}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- HELPERS ----
|
|
214
|
+
function esc(s){return s?String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'):''}
|
|
215
|
+
function fmtTime(iso){if(!iso)return '';try{return new Date(iso).toLocaleTimeString('en',{hour:'2-digit',minute:'2-digit',hour12:true})}catch(e){return iso}}
|
|
216
|
+
|
|
217
|
+
// ---- RENDER ----
|
|
218
|
+
function render(){
|
|
219
|
+
var el=document.getElementById('content');
|
|
220
|
+
if(!el)return;
|
|
221
|
+
switch(currentView){
|
|
222
|
+
case 'dashboard':renderDash(el);break;
|
|
223
|
+
case 'chat':renderChat(el);break;
|
|
224
|
+
case 'plan':renderPlan(el);break;
|
|
225
|
+
case 'tasks':renderTasks(el);break;
|
|
226
|
+
case 'emails':renderEmails(el);break;
|
|
227
|
+
case 'calendar':renderCalendar(el);break;
|
|
228
|
+
case 'agents':renderAgents(el);break;
|
|
401
229
|
}
|
|
402
230
|
}
|
|
403
231
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// ── Dashboard ──────────────────────────────────────────────────────────────
|
|
417
|
-
function renderDashboard(el) {
|
|
418
|
-
const s = dashData.status;
|
|
419
|
-
const tasks = dashData.tasks;
|
|
420
|
-
const emails = dashData.emails;
|
|
421
|
-
const events = dashData.events;
|
|
422
|
-
const done = tasks.filter(t=>t.status==='done').length;
|
|
423
|
-
const pending = tasks.length - done;
|
|
424
|
-
const pct = tasks.length > 0 ? Math.round(done/tasks.length*100) : 0;
|
|
425
|
-
|
|
426
|
-
let statusHtml = '';
|
|
427
|
-
if (s) {
|
|
428
|
-
const dot = s.connected ? 'ok' : 'err';
|
|
429
|
-
statusHtml = '<span class="status-dot '+dot+'"></span>' + (s.connected ? 'Connected' : 'Disconnected') + ' · ' + esc(s.provider || '');
|
|
430
|
-
document.getElementById('header-status').innerHTML = statusHtml;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
el.innerHTML = '<div class="dash-grid">' +
|
|
434
|
-
'<div class="card"><div class="card-title">Tasks Today</div><div class="card-value">'+pending+'</div><div class="card-sub">'+done+' done / '+tasks.length+' total ('+pct+'%)</div></div>' +
|
|
435
|
-
'<div class="card"><div class="card-title">Unread Emails</div><div class="card-value">'+emails.length+'</div><div class="card-sub">'+(emails.length>0 ? esc(emails[0].from) : 'Inbox zero')+'</div></div>' +
|
|
436
|
-
'<div class="card"><div class="card-title">Events Today</div><div class="card-value">'+events.length+'</div><div class="card-sub">'+(events.length>0 ? esc(events[0].summary) : 'No events')+'</div></div>' +
|
|
437
|
-
'<div class="card"><div class="card-title">Agents</div><div class="card-value">38</div><div class="card-sub">Ready to assist</div></div>' +
|
|
438
|
-
'</div>' +
|
|
439
|
-
// Upcoming events
|
|
440
|
-
'<div class="dash-section"><h2>Upcoming Events</h2>' +
|
|
441
|
-
(events.length === 0 ? '<div class="card" style="color:var(--textdim)">No events scheduled for today.</div>' :
|
|
442
|
-
events.slice(0, 5).map(e => '<div class="card" style="padding:10px 14px;display:flex;gap:12px;align-items:center">' +
|
|
443
|
-
'<span style="color:var(--amber);min-width:100px">'+(e.isAllDay ? 'All day' : fmtTime(e.start)+' - '+fmtTime(e.end))+'</span>' +
|
|
444
|
-
'<span style="color:var(--textbright)">'+esc(e.summary)+'</span>' +
|
|
445
|
-
(e.location ? '<span style="color:var(--textdim);font-size:11px"> · '+esc(e.location)+'</span>' : '') +
|
|
446
|
-
'</div>').join('')) +
|
|
447
|
-
'</div>' +
|
|
448
|
-
// Recent emails
|
|
449
|
-
'<div class="dash-section"><h2>Recent Emails</h2>' +
|
|
450
|
-
(emails.length === 0 ? '<div class="card" style="color:var(--textdim)">No unread emails.</div>' :
|
|
451
|
-
emails.slice(0, 5).map(e => '<div class="card" style="padding:10px 14px">' +
|
|
452
|
-
'<div style="display:flex;justify-content:space-between"><span class="email-from">'+esc(e.from)+'</span><span style="color:var(--textdim);font-size:10px">'+esc(e.date)+'</span></div>' +
|
|
453
|
-
'<div style="color:var(--textbright);margin-top:2px">'+esc(e.subject)+'</div>' +
|
|
454
|
-
'<div style="color:var(--textdim);font-size:11px;margin-top:2px">'+esc((e.snippet||'').slice(0,120))+'</div>' +
|
|
455
|
-
'</div>').join('')) +
|
|
456
|
-
'</div>' +
|
|
457
|
-
// Pending tasks
|
|
458
|
-
'<div class="dash-section"><h2>Pending Tasks</h2>' +
|
|
459
|
-
(pending === 0 ? '<div class="card" style="color:var(--textdim)">All tasks complete.</div>' :
|
|
460
|
-
tasks.filter(t=>t.status!=='done').slice(0,5).map(t => '<div class="card" style="padding:10px 14px;display:flex;gap:10px;align-items:center">' +
|
|
461
|
-
'<span class="task-priority '+esc(t.priority)+'">'+esc(t.priority)+'</span>' +
|
|
462
|
-
'<span>'+esc(t.description)+'</span>' +
|
|
463
|
-
'</div>').join('')) +
|
|
232
|
+
// ---- DASHBOARD ----
|
|
233
|
+
function renderDash(el){
|
|
234
|
+
var t=dash.tasks,e=dash.emails,ev=dash.events;
|
|
235
|
+
var done=t.filter(function(x){return x.status==='done'}).length;
|
|
236
|
+
var pend=t.length-done;
|
|
237
|
+
var pct=t.length>0?Math.round(done/t.length*100):0;
|
|
238
|
+
var h='<div class="dash-grid">'+
|
|
239
|
+
'<div class="card"><div class="card__title">Tasks</div><div class="card__value">'+pend+'</div><div class="card__sub">'+done+'/'+t.length+' done ('+pct+'%)</div></div>'+
|
|
240
|
+
'<div class="card"><div class="card__title">Emails</div><div class="card__value">'+e.length+'</div><div class="card__sub">'+(e.length>0?esc(e[0].from):'Inbox zero')+'</div></div>'+
|
|
241
|
+
'<div class="card"><div class="card__title">Events</div><div class="card__value">'+ev.length+'</div><div class="card__sub">'+(ev.length>0?esc(ev[0].summary):'No events')+'</div></div>'+
|
|
242
|
+
'<div class="card"><div class="card__title">Agents</div><div class="card__value">38</div><div class="card__sub">Ready</div></div>'+
|
|
464
243
|
'</div>';
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
'
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
setTimeout(()
|
|
244
|
+
if(ev.length>0){h+='<div class="section-title">Events</div>';ev.slice(0,5).forEach(function(x){h+='<div class="card event"><span class="event__time">'+(x.isAllDay?'All day':fmtTime(x.start)+' - '+fmtTime(x.end))+'</span><span class="event__title">'+esc(x.summary)+'</span>'+(x.location?'<span class="event__location">'+esc(x.location)+'</span>':'')+'</div>'})}
|
|
245
|
+
if(e.length>0){h+='<div class="section-title">Emails</div>';e.slice(0,5).forEach(function(x){h+='<div class="card email"><div class="email__header"><span class="email__from">'+esc(x.from)+'</span><span class="email__date">'+esc(x.date)+'</span></div><div class="email__subject">'+esc(x.subject)+'</div><div class="email__snippet">'+esc((x.snippet||'').slice(0,120))+'</div></div>'})}
|
|
246
|
+
if(pend>0){h+='<div class="section-title">Tasks</div>';t.filter(function(x){return x.status!=='done'}).slice(0,5).forEach(function(x){h+='<div class="card task"><span class="task__priority task__priority--'+esc(x.priority)+'">'+esc(x.priority)+'</span><span class="task__desc">'+esc(x.description)+'</span></div>'})}
|
|
247
|
+
el.innerHTML=h;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- CHAT ----
|
|
251
|
+
var chatReady=false;
|
|
252
|
+
function renderChat(el){
|
|
253
|
+
if(!chatReady||!document.getElementById('chatMessages')){
|
|
254
|
+
el.innerHTML='<div class="chat"><div class="chat__messages" id="chatMessages"></div><div class="chat__bar"><textarea class="chat__input" id="chatInput" placeholder="Ask anything..." rows="1"></textarea><button class="chat__send" id="chatSend">Send</button></div></div>';
|
|
255
|
+
chatReady=true;
|
|
256
|
+
document.getElementById('chatSend').onclick=sendChat;
|
|
257
|
+
document.getElementById('chatInput').onkeydown=function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}};
|
|
258
|
+
renderMessages();
|
|
259
|
+
setTimeout(function(){var i=document.getElementById('chatInput');if(i)i.focus()},100);
|
|
481
260
|
}
|
|
482
261
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (chatHistory.length === 0) {
|
|
488
|
-
el.innerHTML = '<div style="text-align:center;padding:60px 20px;color:var(--textdim)">' +
|
|
489
|
-
'<div style="font-size:32px;margin-bottom:16px;color:var(--green)">NHA Chat</div>' +
|
|
490
|
-
'<div>Personal Operations Assistant</div>' +
|
|
491
|
-
'<div style="margin-top:12px;font-size:11px">Try: "Show my unread emails" / "What is on my calendar?" / "Add a task to review the PR"</div>' +
|
|
492
|
-
'</div>';
|
|
262
|
+
function renderMessages(){
|
|
263
|
+
var el=document.getElementById('chatMessages');if(!el)return;
|
|
264
|
+
if(chatHistory.length===0){
|
|
265
|
+
el.innerHTML='<div class="chat__empty"><div class="chat__empty-title">NHA Chat</div><div>Personal Operations Assistant</div><div class="chat__empty-hint">Try: Show my unread emails / What is on my calendar? / Add a task</div></div>';
|
|
493
266
|
return;
|
|
494
267
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (chatBusy) return;
|
|
513
|
-
const input = document.getElementById('chat-input');
|
|
514
|
-
const text = input.value.trim();
|
|
515
|
-
if (!text) return;
|
|
516
|
-
input.value = '';
|
|
517
|
-
|
|
518
|
-
chatHistory.push({role:'user', content:text});
|
|
519
|
-
chatHistory.push({role:'assistant', content:'Thinking...'});
|
|
520
|
-
renderChatMessages();
|
|
521
|
-
chatBusy = true;
|
|
522
|
-
document.getElementById('chat-send').disabled = true;
|
|
523
|
-
|
|
524
|
-
const data = await apiPost('/api/chat', {message: text, history: chatHistory.slice(0,-2)});
|
|
525
|
-
chatHistory.pop(); // remove thinking
|
|
526
|
-
if (data?.response) {
|
|
527
|
-
chatHistory.push({role:'assistant', content: data.response});
|
|
528
|
-
if (data.toolResult) {
|
|
529
|
-
chatHistory.push({role:'assistant', content: '[Tool Result]\\n' + data.toolResult});
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
chatHistory.push({role:'assistant', content: data?.error || 'Error: could not get response.'});
|
|
533
|
-
}
|
|
534
|
-
renderChatMessages();
|
|
535
|
-
chatBusy = false;
|
|
536
|
-
document.getElementById('chat-send').disabled = false;
|
|
537
|
-
input.focus();
|
|
538
|
-
// Refresh dashboard data after chat (chat may have changed tasks/emails)
|
|
539
|
-
loadDashboard();
|
|
268
|
+
var h='';chatHistory.forEach(function(m){
|
|
269
|
+
h+='<div class="msg msg--'+esc(m.role)+'"><div class="msg__label">'+esc(m.role==='user'?'You':'NHA')+'</div><div class="msg__bubble">'+esc(m.content)+'</div></div>';
|
|
270
|
+
});
|
|
271
|
+
el.innerHTML=h;el.scrollTop=el.scrollHeight;
|
|
272
|
+
}
|
|
273
|
+
function sendChat(){
|
|
274
|
+
var inp=document.getElementById('chatInput');if(!inp)return;
|
|
275
|
+
var msg=inp.value.trim();if(!msg)return;
|
|
276
|
+
chatHistory.push({role:'user',content:msg});
|
|
277
|
+
inp.value='';renderMessages();
|
|
278
|
+
chatHistory.push({role:'assistant',content:'Thinking...'});renderMessages();
|
|
279
|
+
apiPost('/api/chat',{message:msg,history:chatHistory.slice(0,-1)}).then(function(r){
|
|
280
|
+
chatHistory.pop();
|
|
281
|
+
if(r&&r.response){chatHistory.push({role:'assistant',content:r.response})}
|
|
282
|
+
else{chatHistory.push({role:'assistant',content:'Error: no response'})}
|
|
283
|
+
renderMessages();
|
|
284
|
+
});
|
|
540
285
|
}
|
|
541
286
|
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
287
|
+
// ---- TASKS ----
|
|
288
|
+
function renderTasks(el){
|
|
289
|
+
var t=dash.tasks;
|
|
290
|
+
var h='<div class="task-bar"><input id="taskInput" placeholder="Add a new task..."><select id="taskPriority"><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option><option value="low">Low</option></select><button onclick="addTaskUI()">Add</button></div>';
|
|
291
|
+
t.sort(function(a,b){if(a.status==='done'&&b.status!=='done')return 1;if(a.status!=='done'&&b.status==='done')return -1;return 0});
|
|
292
|
+
t.forEach(function(x){
|
|
293
|
+
var isDone=x.status==='done';
|
|
294
|
+
h+='<div class="card task'+(isDone?' task--done':'')+'"><span class="task__check'+(isDone?' task__check--done':'')+'" onclick="toggleTask('+x.id+')">'+(isDone?'\\u2713':'')+'</span><span class="task__desc">'+esc(x.description)+'</span><span class="task__priority task__priority--'+esc(x.priority)+'">'+esc(x.priority)+'</span></div>';
|
|
295
|
+
});
|
|
296
|
+
el.innerHTML=h;
|
|
297
|
+
var inp=document.getElementById('taskInput');
|
|
298
|
+
if(inp)inp.onkeydown=function(e){if(e.key==='Enter')addTaskUI()};
|
|
299
|
+
}
|
|
300
|
+
function addTaskUI(){
|
|
301
|
+
var inp=document.getElementById('taskInput'),sel=document.getElementById('taskPriority');
|
|
302
|
+
if(!inp||!inp.value.trim())return;
|
|
303
|
+
apiPost('/api/tasks',{description:inp.value.trim(),priority:sel?sel.value:'medium'}).then(function(){
|
|
304
|
+
inp.value='';loadDash().then(function(){if(currentView==='tasks')render()});
|
|
305
|
+
});
|
|
554
306
|
}
|
|
555
|
-
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
html += '</div>';
|
|
574
|
-
}
|
|
575
|
-
html += '</div>';
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Schedule
|
|
579
|
-
if (plan.schedule?.length) {
|
|
580
|
-
html += '<div class="plan-section"><h3>Schedule</h3>';
|
|
581
|
-
for (const s of plan.schedule) {
|
|
582
|
-
html += '<div class="plan-schedule-item">' +
|
|
583
|
-
'<span class="plan-schedule-time">'+esc(s.time_start)+' - '+esc(s.time_end)+'</span>' +
|
|
584
|
-
'<span class="plan-schedule-type '+(s.type||'task')+'">'+esc(s.type||'task')+'</span>' +
|
|
585
|
-
'<span style="flex:1"><span style="color:var(--textbright)">'+esc(s.title)+'</span>' +
|
|
586
|
-
(s.notes ? '<div style="color:var(--textdim);font-size:11px;margin-top:2px">'+esc(s.notes)+'</div>' : '') +
|
|
587
|
-
(s.preparation ? '<div style="color:var(--amber);font-size:11px;margin-top:2px">Prep: '+esc(s.preparation)+'</div>' : '') +
|
|
588
|
-
'</span></div>';
|
|
589
|
-
}
|
|
590
|
-
html += '</div>';
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Email actions
|
|
594
|
-
if (plan.email_actions?.length) {
|
|
595
|
-
html += '<div class="plan-section"><h3>Email Actions</h3>';
|
|
596
|
-
for (const e of plan.email_actions) {
|
|
597
|
-
const ac = e.action === 'reply' ? 'color:var(--green)' : e.action === 'flag' ? 'color:var(--amber)' : 'color:var(--textdim)';
|
|
598
|
-
html += '<div style="padding:6px 0"><span style="'+ac+';font-weight:600;text-transform:uppercase;font-size:11px">['+esc(e.action)+']</span> '+esc(e.subject)+' <span style="color:var(--textdim)">from '+esc(e.from)+'</span></div>';
|
|
599
|
-
}
|
|
600
|
-
html += '</div>';
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Security alerts
|
|
604
|
-
if (plan.security_alerts?.length) {
|
|
605
|
-
html += '<div class="plan-section"><h3>Security Alerts</h3>';
|
|
606
|
-
for (const a of plan.security_alerts) {
|
|
607
|
-
html += '<div class="plan-alert">'+esc(typeof a === 'string' ? a : a.message || JSON.stringify(a))+'</div>';
|
|
608
|
-
}
|
|
609
|
-
html += '</div>';
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Insights
|
|
613
|
-
if (plan.insights?.length) {
|
|
614
|
-
html += '<div class="plan-section"><h3>Insights</h3>';
|
|
615
|
-
for (const i of plan.insights) {
|
|
616
|
-
html += '<div class="plan-insight">→ '+esc(typeof i === 'string' ? i : i.message || JSON.stringify(i))+'</div>';
|
|
617
|
-
}
|
|
618
|
-
html += '</div>';
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Metadata
|
|
622
|
-
if (plan.metadata) {
|
|
623
|
-
html += '<div class="plan-meta">Generated in '+(plan.metadata.durationMs/1000).toFixed(1)+'s by '+(plan.metadata.agentsUsed?.length||0)+' agents ('+esc(plan.metadata.provider)+')</div>';
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
el.innerHTML = html;
|
|
307
|
+
function toggleTask(id){
|
|
308
|
+
apiPatch('/api/tasks/'+id+'/done').then(function(){loadDash().then(function(){if(currentView==='tasks')render()})});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---- PLAN ----
|
|
312
|
+
function renderPlan(el){
|
|
313
|
+
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading plan...</div></div>';
|
|
314
|
+
apiGet('/api/plan').then(function(r){
|
|
315
|
+
var p=r&&r.plan;
|
|
316
|
+
if(!p){el.innerHTML='<div class="card" style="text-align:center;padding:30px"><div style="color:var(--dim);margin-bottom:12px">No plan generated yet.</div><button class="btn btn--primary" onclick="refreshPlan()">Generate Plan</button></div>';return}
|
|
317
|
+
var h='<div class="plan-summary">'+esc(p.executive_summary||'No summary')+'</div>';
|
|
318
|
+
if(p.priority_actions&&p.priority_actions.length>0){h+='<div class="section-title">Priority Actions</div>';p.priority_actions.forEach(function(a){h+='<div class="card plan-action"><span class="plan-action__time">'+esc(a.time||'')+'</span><span class="plan-action__text">'+esc(a.action)+'</span></div>'})}
|
|
319
|
+
if(p.schedule&&p.schedule.length>0){h+='<div class="section-title">Schedule</div>';p.schedule.forEach(function(s){h+='<div class="card event"><span class="event__time">'+esc(s.time_start)+'-'+esc(s.time_end)+'</span><span class="event__title">'+esc(s.title)+'</span></div>'})}
|
|
320
|
+
if(p.security_alerts&&p.security_alerts.length>0){h+='<div class="section-title" style="color:var(--red)">Security Alerts</div>';p.security_alerts.forEach(function(a){h+='<div class="card" style="border-color:var(--red)"><span style="color:var(--red)">'+esc(typeof a==='string'?a:a.message||JSON.stringify(a))+'</span></div>'})}
|
|
321
|
+
if(p.insights&&p.insights.length>0){h+='<div class="section-title">Insights</div>';p.insights.forEach(function(i){h+='<div style="color:var(--dim);padding:4px 0;font-size:12px">\\u2192 '+esc(typeof i==='string'?i:i.message||'')+'</div>'})}
|
|
322
|
+
h+='<div style="margin-top:16px;text-align:center"><button class="btn btn--secondary" onclick="refreshPlan()">Regenerate</button></div>';
|
|
323
|
+
el.innerHTML=h;
|
|
324
|
+
});
|
|
627
325
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const data = await apiPost('/api/plan/refresh', {});
|
|
633
|
-
dashData.plan = data?.plan || null;
|
|
634
|
-
if (currentView === 'plan') renderPlan(el);
|
|
326
|
+
function refreshPlan(){
|
|
327
|
+
var el=document.getElementById('content');
|
|
328
|
+
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Generating plan with 5 agents...</div></div>';
|
|
329
|
+
apiPost('/api/plan/refresh',{}).then(function(){renderPlan(el)});
|
|
635
330
|
}
|
|
636
331
|
|
|
637
|
-
//
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
'<
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (tasks.length === 0) {
|
|
647
|
-
html += '<div class="card" style="text-align:center;padding:30px;color:var(--textdim)">No tasks for today. Add one above.</div>';
|
|
648
|
-
} else {
|
|
649
|
-
// Pending first, then done
|
|
650
|
-
const sorted = [...tasks].sort((a,b) => {
|
|
651
|
-
if (a.status==='done' && b.status!=='done') return 1;
|
|
652
|
-
if (a.status!=='done' && b.status==='done') return -1;
|
|
653
|
-
const pr = {critical:0,high:1,medium:2,low:3};
|
|
654
|
-
return (pr[a.priority]||2) - (pr[b.priority]||2);
|
|
655
|
-
});
|
|
656
|
-
for (const t of sorted) {
|
|
657
|
-
const isDone = t.status === 'done';
|
|
658
|
-
html += '<div class="task-item">' +
|
|
659
|
-
'<div class="task-check'+(isDone?' done':'')+'" onclick="toggleTask('+t.id+','+isDone+')"></div>' +
|
|
660
|
-
'<span class="task-desc'+(isDone?' done':'')+'">'+esc(t.description)+'</span>' +
|
|
661
|
-
'<span class="task-priority '+esc(t.priority)+'">'+esc(t.priority)+'</span>' +
|
|
662
|
-
'<span style="color:var(--textdim);font-size:10px">#'+t.id+'</span>' +
|
|
663
|
-
'</div>';
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
el.innerHTML = html;
|
|
332
|
+
// ---- EMAILS ----
|
|
333
|
+
function renderEmails(el){
|
|
334
|
+
var e=dash.emails;
|
|
335
|
+
if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No unread emails</div>';return}
|
|
336
|
+
var h='';e.forEach(function(x){
|
|
337
|
+
h+='<div class="card email"><div class="email__header"><span class="email__from">'+esc(x.from)+'</span><span class="email__date">'+esc(x.date)+'</span></div><div class="email__subject">'+esc(x.subject)+'</div><div class="email__snippet">'+esc((x.snippet||'').slice(0,150))+'</div></div>';
|
|
338
|
+
});
|
|
339
|
+
el.innerHTML=h;
|
|
667
340
|
}
|
|
668
341
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
descEl.value = '';
|
|
678
|
-
renderTasks(document.getElementById('content'));
|
|
679
|
-
updateBadges();
|
|
680
|
-
}
|
|
342
|
+
// ---- CALENDAR ----
|
|
343
|
+
function renderCalendar(el){
|
|
344
|
+
var ev=dash.events;
|
|
345
|
+
if(ev.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">No events today</div>';return}
|
|
346
|
+
var h='';ev.forEach(function(x){
|
|
347
|
+
h+='<div class="card event"><span class="event__time">'+(x.isAllDay?'All day':fmtTime(x.start)+' - '+fmtTime(x.end))+'</span><span class="event__title">'+esc(x.summary)+'</span>'+(x.location?'<span class="event__location">'+esc(x.location)+'</span>':'')+'</div>';
|
|
348
|
+
});
|
|
349
|
+
el.innerHTML=h;
|
|
681
350
|
}
|
|
682
351
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
352
|
+
// ---- AGENTS ----
|
|
353
|
+
function renderAgents(el){
|
|
354
|
+
if(agentsList.length===0){el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading agents...</div></div>';loadAgents().then(function(){renderAgents(el)});return}
|
|
355
|
+
var h='<div class="agents-grid">';
|
|
356
|
+
agentsList.forEach(function(a){
|
|
357
|
+
h+='<div class="card agent-card" onclick="openAgent(\\''+esc(a.name||a.agentName)+'\\',\\''+esc(a.displayName||a.name)+'\\')"><div class="agent-card__name">'+esc(a.displayName||a.name)+'</div><div class="agent-card__cat">'+esc(a.category||'')+'</div></div>';
|
|
358
|
+
});
|
|
359
|
+
h+='</div>';
|
|
360
|
+
el.innerHTML=h;
|
|
361
|
+
}
|
|
362
|
+
function openAgent(name,display){
|
|
363
|
+
selectedAgent=name;
|
|
364
|
+
document.getElementById('modalName').textContent=display||name;
|
|
365
|
+
document.getElementById('modalPrompt').value='';
|
|
366
|
+
document.getElementById('modalResponse').style.display='none';
|
|
367
|
+
document.getElementById('modalResponse').textContent='';
|
|
368
|
+
document.getElementById('agentModal').classList.add('modal-overlay--open');
|
|
369
|
+
}
|
|
370
|
+
function closeModal(){
|
|
371
|
+
document.getElementById('agentModal').classList.remove('modal-overlay--open');
|
|
372
|
+
}
|
|
373
|
+
function askAgent(){
|
|
374
|
+
var p=document.getElementById('modalPrompt').value.trim();if(!p||!selectedAgent)return;
|
|
375
|
+
var resp=document.getElementById('modalResponse');
|
|
376
|
+
resp.style.display='block';resp.textContent='Thinking...';
|
|
377
|
+
apiPost('/api/ask',{agent:selectedAgent,prompt:p}).then(function(r){
|
|
378
|
+
resp.textContent=(r&&r.response)||'Error: no response';
|
|
379
|
+
});
|
|
692
380
|
}
|
|
693
381
|
|
|
694
|
-
//
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
if
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
el.innerHTML = emails.map(e => '<div class="email-item">' +
|
|
702
|
-
'<div style="display:flex;justify-content:space-between"><span class="email-from">'+esc(e.from)+'</span><span class="email-date">'+esc(e.date)+'</span></div>' +
|
|
703
|
-
'<div class="email-subject">'+esc(e.subject)+'</div>' +
|
|
704
|
-
'<div class="email-snippet">'+esc((e.snippet||'').slice(0,200))+'</div>' +
|
|
705
|
-
'</div>').join('');
|
|
382
|
+
// ---- INIT ----
|
|
383
|
+
function init(){
|
|
384
|
+
var el=document.getElementById('content');
|
|
385
|
+
if(el)el.innerHTML='<div style="display:flex;align-items:center;justify-content:center;height:50vh;flex-direction:column"><div class="spinner"></div><div style="color:var(--dim)">Loading...</div></div>';
|
|
386
|
+
loadDash().then(function(){render()}).catch(function(){render()});
|
|
387
|
+
loadAgents().catch(function(){});
|
|
388
|
+
setInterval(function(){loadDash().then(function(){if(currentView==='dashboard')render()}).catch(function(){})},120000);
|
|
706
389
|
}
|
|
390
|
+
init();
|
|
391
|
+
`;
|
|
707
392
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
(e.hangoutLink ? '<div><a href="'+esc(e.hangoutLink)+'" target="_blank" style="font-size:11px">Join call</a></div>' : '') +
|
|
724
|
-
'</div>' +
|
|
725
|
-
'</div>';
|
|
726
|
-
}).join('');
|
|
727
|
-
}
|
|
393
|
+
return `<!DOCTYPE html>
|
|
394
|
+
<html lang="en">
|
|
395
|
+
<head>
|
|
396
|
+
<meta charset="utf-8">
|
|
397
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
398
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
399
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
400
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
401
|
+
<meta name="apple-mobile-web-app-title" content="NHA">
|
|
402
|
+
<meta name="theme-color" content="#0a0a0a">
|
|
403
|
+
<link rel="manifest" href="/manifest.json">
|
|
404
|
+
<title>NHA</title>
|
|
405
|
+
<style>${CSS}</style>
|
|
406
|
+
</head>
|
|
407
|
+
<body>
|
|
728
408
|
|
|
729
|
-
|
|
730
|
-
function renderAgents(el) {
|
|
731
|
-
if (agentsList.length === 0) {
|
|
732
|
-
el.innerHTML = '<div class="loading"><div class="spinner"></div><div>Loading agents...</div></div>';
|
|
733
|
-
loadAgents().then(() => { if (currentView === 'agents') renderAgents(el); });
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
el.innerHTML = '<div class="agents-grid">' + agentsList.map(a =>
|
|
737
|
-
'<div class="agent-card" data-agent="'+esc(a.name)+'" onclick="openAgent(this.dataset.agent)">' +
|
|
738
|
-
'<div class="agent-name">'+esc(a.name)+'</div>' +
|
|
739
|
-
'<div class="agent-category">'+esc(a.category || 'agent')+'</div>' +
|
|
740
|
-
'<div class="agent-tagline">'+esc(a.tagline || '')+'</div>' +
|
|
741
|
-
'</div>'
|
|
742
|
-
).join('') + '</div>';
|
|
743
|
-
}
|
|
409
|
+
<div class="sidebar__overlay" id="overlay" onclick="closeSidebar()"></div>
|
|
744
410
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
411
|
+
<div class="app">
|
|
412
|
+
<nav class="sidebar" id="sidebar">
|
|
413
|
+
<div class="sidebar__brand">
|
|
414
|
+
<div class="sidebar__brand-name">NHA</div>
|
|
415
|
+
<div class="sidebar__brand-sub">Operations Console</div>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="sidebar__section">
|
|
418
|
+
<div class="sidebar__label">Overview</div>
|
|
419
|
+
<div class="nav-item nav-item--active" data-view="dashboard" onclick="switchView('dashboard')"><span class="nav-item__icon">■</span> Dashboard</div>
|
|
420
|
+
<div class="nav-item" data-view="chat" onclick="switchView('chat')"><span class="nav-item__icon">✉</span> Chat</div>
|
|
421
|
+
<div class="nav-item" data-view="plan" onclick="switchView('plan')"><span class="nav-item__icon">★</span> Plan</div>
|
|
422
|
+
<div class="nav-item" data-view="tasks" onclick="switchView('tasks')"><span class="nav-item__icon">☑</span> Tasks <span class="nav-item__badge" id="taskBadge" style="display:none">0</span></div>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="sidebar__section">
|
|
425
|
+
<div class="sidebar__label">Data</div>
|
|
426
|
+
<div class="nav-item" data-view="emails" onclick="switchView('emails')"><span class="nav-item__icon">✉</span> Emails <span class="nav-item__badge" id="emailBadge" style="display:none">0</span></div>
|
|
427
|
+
<div class="nav-item" data-view="calendar" onclick="switchView('calendar')"><span class="nav-item__icon">📅</span> Calendar</div>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="sidebar__section">
|
|
430
|
+
<div class="sidebar__label">AI</div>
|
|
431
|
+
<div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">⚙</span> Agents</div>
|
|
432
|
+
</div>
|
|
433
|
+
</nav>
|
|
754
434
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
435
|
+
<div class="header">
|
|
436
|
+
<button class="header__burger" onclick="toggleSidebar()">☰</button>
|
|
437
|
+
<span class="header__title" id="headerTitle">Dashboard</span>
|
|
438
|
+
<span class="header__clock" id="clock"></span>
|
|
439
|
+
</div>
|
|
759
440
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const prompt = document.getElementById('modal-prompt').value.trim();
|
|
763
|
-
if (!prompt) return;
|
|
764
|
-
const respEl = document.getElementById('modal-response');
|
|
765
|
-
const sendBtn = document.getElementById('modal-send');
|
|
766
|
-
respEl.style.display = 'block';
|
|
767
|
-
respEl.textContent = 'Thinking...';
|
|
768
|
-
sendBtn.disabled = true;
|
|
769
|
-
|
|
770
|
-
const data = await apiPost('/api/ask', {agent: selectedAgent, prompt});
|
|
771
|
-
if (data?.response) {
|
|
772
|
-
respEl.textContent = data.response;
|
|
773
|
-
} else {
|
|
774
|
-
respEl.textContent = data?.error || 'Error getting response.';
|
|
775
|
-
}
|
|
776
|
-
sendBtn.disabled = false;
|
|
777
|
-
}
|
|
441
|
+
<div class="content" id="content"></div>
|
|
442
|
+
</div>
|
|
778
443
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
try {
|
|
796
|
-
await loadDashboard();
|
|
797
|
-
} catch (e) {
|
|
798
|
-
console.error('Dashboard load error:', e);
|
|
799
|
-
}
|
|
800
|
-
renderView();
|
|
801
|
-
try {
|
|
802
|
-
loadPlan().catch(()=>{});
|
|
803
|
-
loadAgents().catch(()=>{});
|
|
804
|
-
} catch {}
|
|
805
|
-
// Auto-refresh every 2 minutes
|
|
806
|
-
setInterval(() => loadDashboard().then(() => { if (currentView === 'dashboard') renderView(); }).catch(()=>{}), 120000);
|
|
807
|
-
}
|
|
444
|
+
<div class="modal-overlay" id="agentModal">
|
|
445
|
+
<div class="modal">
|
|
446
|
+
<div class="modal__header">
|
|
447
|
+
<h2 id="modalName">Agent</h2>
|
|
448
|
+
<button class="modal__close" onclick="closeModal()">×</button>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="modal__body">
|
|
451
|
+
<textarea id="modalPrompt" placeholder="Ask this agent something..."></textarea>
|
|
452
|
+
<div class="modal__response" id="modalResponse" style="display:none"></div>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="modal__footer">
|
|
455
|
+
<button class="btn btn--secondary" onclick="closeModal()">Close</button>
|
|
456
|
+
<button class="btn btn--primary" onclick="askAgent()">Ask</button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
808
460
|
|
|
809
|
-
|
|
810
|
-
</script>
|
|
461
|
+
<script>${JS}</script>
|
|
811
462
|
</body>
|
|
812
463
|
</html>`;
|
|
813
464
|
}
|