AzraelH commited on
Commit
73908b6
·
1 Parent(s): be5e35d

Replace Gradio UI with native web page

Browse files
Files changed (1) hide show
  1. server/app.py +367 -4
server/app.py CHANGED
@@ -2,9 +2,11 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import uvicorn
6
- from fastapi.responses import JSONResponse
7
- from openenv.core import create_app
8
 
9
  try:
10
  from ..models import EngineerManagerAction, EngineerManagerObservation
@@ -20,14 +22,375 @@ except ImportError:
20
  from server.engineer_manager_environment import EngineerManagerEnvironment
21
 
22
 
23
- app = create_app(
24
  EngineerManagerEnvironment,
25
  EngineerManagerAction,
26
  EngineerManagerObservation,
27
- env_name="engineer-manager",
28
  max_concurrent_envs=2,
29
  )
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  @app.get("/manifest.json", include_in_schema=False)
33
  def manifest() -> JSONResponse:
 
2
 
3
  from __future__ import annotations
4
 
5
+ from textwrap import dedent
6
+
7
  import uvicorn
8
+ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
9
+ from openenv.core.env_server.http_server import create_fastapi_app
10
 
11
  try:
12
  from ..models import EngineerManagerAction, EngineerManagerObservation
 
22
  from server.engineer_manager_environment import EngineerManagerEnvironment
23
 
24
 
25
+ app = create_fastapi_app(
26
  EngineerManagerEnvironment,
27
  EngineerManagerAction,
28
  EngineerManagerObservation,
 
29
  max_concurrent_envs=2,
30
  )
31
 
32
+ WEB_PAGE = dedent(
33
+ """\
34
+ <!doctype html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <title>Engineer Manager</title>
40
+ <style>
41
+ :root {
42
+ color-scheme: dark;
43
+ --bg: #07111f;
44
+ --panel: rgba(10, 21, 38, 0.88);
45
+ --panel-2: rgba(14, 28, 48, 0.96);
46
+ --line: rgba(124, 231, 255, 0.18);
47
+ --text: #eaf4ff;
48
+ --muted: #99abc7;
49
+ --accent: #7ce7ff;
50
+ --accent-2: #86f0ca;
51
+ --meeting: #f5c76a;
52
+ --empty: rgba(255, 255, 255, 0.06);
53
+ --danger: #ff8d8d;
54
+ }
55
+ * { box-sizing: border-box; }
56
+ body {
57
+ margin: 0;
58
+ min-height: 100vh;
59
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
60
+ color: var(--text);
61
+ background:
62
+ radial-gradient(circle at top, rgba(124, 231, 255, 0.12), transparent 30rem),
63
+ linear-gradient(180deg, #09111d 0%, #07111f 100%);
64
+ }
65
+ .shell {
66
+ max-width: 1180px;
67
+ margin: 0 auto;
68
+ padding: 32px 20px 40px;
69
+ }
70
+ .hero {
71
+ padding: 24px;
72
+ border: 1px solid var(--line);
73
+ border-radius: 24px;
74
+ background: linear-gradient(135deg, rgba(9, 20, 38, 0.95), rgba(12, 31, 54, 0.92));
75
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
76
+ }
77
+ .eyebrow {
78
+ color: var(--accent);
79
+ font-size: 12px;
80
+ letter-spacing: 0.18em;
81
+ text-transform: uppercase;
82
+ margin-bottom: 10px;
83
+ }
84
+ h1 {
85
+ margin: 0 0 10px;
86
+ font-size: clamp(2rem, 5vw, 3.8rem);
87
+ line-height: 0.95;
88
+ }
89
+ .sub {
90
+ margin: 0;
91
+ color: var(--muted);
92
+ max-width: 62ch;
93
+ line-height: 1.5;
94
+ }
95
+ .grid {
96
+ display: grid;
97
+ grid-template-columns: 300px minmax(0, 1fr);
98
+ gap: 18px;
99
+ margin-top: 18px;
100
+ }
101
+ .panel {
102
+ border: 1px solid var(--line);
103
+ border-radius: 22px;
104
+ background: var(--panel);
105
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.22);
106
+ backdrop-filter: blur(10px);
107
+ }
108
+ .panel-inner { padding: 18px; }
109
+ .section-title {
110
+ margin: 0 0 14px;
111
+ font-size: 0.92rem;
112
+ letter-spacing: 0.12em;
113
+ text-transform: uppercase;
114
+ color: var(--accent);
115
+ }
116
+ .metrics {
117
+ display: grid;
118
+ grid-template-columns: repeat(4, minmax(0, 1fr));
119
+ gap: 12px;
120
+ margin-bottom: 16px;
121
+ }
122
+ .metric {
123
+ padding: 14px;
124
+ border-radius: 18px;
125
+ border: 1px solid var(--line);
126
+ background: var(--panel-2);
127
+ }
128
+ .metric small {
129
+ display: block;
130
+ color: var(--muted);
131
+ margin-bottom: 8px;
132
+ text-transform: uppercase;
133
+ letter-spacing: 0.08em;
134
+ font-size: 0.7rem;
135
+ }
136
+ .metric strong {
137
+ font-size: 1.35rem;
138
+ }
139
+ label {
140
+ display: block;
141
+ margin-bottom: 10px;
142
+ color: var(--muted);
143
+ font-size: 0.92rem;
144
+ }
145
+ input, select, button {
146
+ width: 100%;
147
+ border-radius: 14px;
148
+ border: 1px solid var(--line);
149
+ background: rgba(255, 255, 255, 0.04);
150
+ color: var(--text);
151
+ padding: 12px 14px;
152
+ font: inherit;
153
+ }
154
+ button {
155
+ cursor: pointer;
156
+ transition: transform 120ms ease, background 120ms ease;
157
+ background: linear-gradient(135deg, rgba(124, 231, 255, 0.22), rgba(134, 240, 202, 0.16));
158
+ }
159
+ button:hover { transform: translateY(-1px); }
160
+ .actions {
161
+ display: grid;
162
+ grid-template-columns: repeat(2, minmax(0, 1fr));
163
+ gap: 10px;
164
+ margin-top: 12px;
165
+ }
166
+ .actions button[data-op="0"] { grid-column: span 2; }
167
+ .timeline {
168
+ display: grid;
169
+ grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
170
+ gap: 8px;
171
+ }
172
+ .slot {
173
+ border-radius: 16px;
174
+ min-height: 84px;
175
+ padding: 10px 8px;
176
+ border: 1px solid transparent;
177
+ background: var(--empty);
178
+ display: flex;
179
+ flex-direction: column;
180
+ justify-content: space-between;
181
+ }
182
+ .slot.current { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(124, 231, 255, 0.25) inset; }
183
+ .slot.work { background: linear-gradient(180deg, rgba(124, 240, 202, 0.22), rgba(124, 240, 202, 0.10)); }
184
+ .slot.meeting { background: linear-gradient(180deg, rgba(245, 199, 106, 0.28), rgba(245, 199, 106, 0.10)); }
185
+ .slot small { color: var(--muted); }
186
+ .slot strong { font-size: 0.82rem; }
187
+ .task-list, pre {
188
+ margin: 0;
189
+ padding: 14px;
190
+ border-radius: 18px;
191
+ border: 1px solid var(--line);
192
+ background: rgba(255, 255, 255, 0.03);
193
+ }
194
+ .task-list li { margin: 0 0 8px; color: var(--muted); }
195
+ .task-list li:last-child { margin-bottom: 0; }
196
+ pre {
197
+ overflow: auto;
198
+ max-height: 260px;
199
+ color: #d7e6ff;
200
+ }
201
+ .status {
202
+ margin-top: 12px;
203
+ min-height: 24px;
204
+ color: var(--muted);
205
+ }
206
+ .status.error { color: var(--danger); }
207
+ @media (max-width: 920px) {
208
+ .grid, .metrics { grid-template-columns: 1fr; }
209
+ .actions { grid-template-columns: 1fr; }
210
+ .actions button[data-op="0"] { grid-column: span 1; }
211
+ }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="shell">
216
+ <section class="hero">
217
+ <div class="eyebrow">Engineer Manager Simulator</div>
218
+ <h1>Run the day before the day runs you.</h1>
219
+ <p class="sub">This is a direct native UI for the Space. It talks to the environment API without the Gradio bridge, so it avoids the Hugging Face wrapper errors that were breaking the previous page.</p>
220
+ </section>
221
+
222
+ <div class="grid">
223
+ <aside class="panel">
224
+ <div class="panel-inner">
225
+ <h2 class="section-title">Controls</h2>
226
+ <label>Target slot
227
+ <input id="targetSlot" type="number" min="0" value="0">
228
+ </label>
229
+ <button id="resetBtn">Reset Environment</button>
230
+ <div class="actions">
231
+ <button data-op="0">Advance Time</button>
232
+ <button data-op="1">Schedule Work</button>
233
+ <button data-op="2">Move Meeting</button>
234
+ <button data-op="3">Mute Comms</button>
235
+ </div>
236
+ <div id="status" class="status">Ready.</div>
237
+ </div>
238
+ </aside>
239
+
240
+ <main class="panel">
241
+ <div class="panel-inner">
242
+ <div class="metrics">
243
+ <div class="metric"><small>Current Time</small><strong id="m-time">-</strong></div>
244
+ <div class="metric"><small>Current Slot</small><strong id="m-slot">-</strong></div>
245
+ <div class="metric"><small>Flow Score</small><strong id="m-flow">-</strong></div>
246
+ <div class="metric"><small>Social Debt</small><strong id="m-debt">-</strong></div>
247
+ </div>
248
+
249
+ <h2 class="section-title">Timeline</h2>
250
+ <div id="timeline" class="timeline"></div>
251
+
252
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px;">
253
+ <section>
254
+ <h2 class="section-title">Task Buffer</h2>
255
+ <ul id="tasks" class="task-list"></ul>
256
+ </section>
257
+ <section>
258
+ <h2 class="section-title">Last Response</h2>
259
+ <pre id="payload">{}</pre>
260
+ </section>
261
+ </div>
262
+ </div>
263
+ </main>
264
+ </div>
265
+ </div>
266
+
267
+ <script>
268
+ const statusEl = document.getElementById("status");
269
+ const targetSlotEl = document.getElementById("targetSlot");
270
+ const timelineEl = document.getElementById("timeline");
271
+ const tasksEl = document.getElementById("tasks");
272
+ const payloadEl = document.getElementById("payload");
273
+ const metrics = {
274
+ time: document.getElementById("m-time"),
275
+ slot: document.getElementById("m-slot"),
276
+ flow: document.getElementById("m-flow"),
277
+ debt: document.getElementById("m-debt"),
278
+ };
279
+
280
+ function setStatus(message, isError = false) {
281
+ statusEl.textContent = message;
282
+ statusEl.className = isError ? "status error" : "status";
283
+ }
284
+
285
+ function kindLabel(value) {
286
+ if (value === 1) return "Deep Work";
287
+ if (value === 2) return "Meeting";
288
+ return "Open";
289
+ }
290
+
291
+ function renderObservation(response) {
292
+ const obs = response.observation || {};
293
+ metrics.time.textContent = obs.current_time ?? "-";
294
+ metrics.slot.textContent = String(obs.current_slot ?? "-");
295
+ metrics.flow.textContent = Number(obs.flow_score ?? 0).toFixed(2);
296
+ metrics.debt.textContent = Number(obs.social_debt ?? 0).toFixed(2);
297
+
298
+ targetSlotEl.max = Math.max(0, (obs.timeline || []).length - 1);
299
+
300
+ timelineEl.innerHTML = "";
301
+ (obs.timeline || []).forEach((value, index) => {
302
+ const slot = document.createElement("div");
303
+ slot.className = "slot " + (value === 1 ? "work" : value === 2 ? "meeting" : "");
304
+ if (index === obs.current_slot) slot.classList.add("current");
305
+ slot.innerHTML = "<small>Slot " + index + "</small><strong>" + kindLabel(value) + "</strong>";
306
+ timelineEl.appendChild(slot);
307
+ });
308
+
309
+ tasksEl.innerHTML = "";
310
+ const tasks = obs.task_buffer || [];
311
+ if (!tasks.length) {
312
+ const empty = document.createElement("li");
313
+ empty.textContent = "No queued tasks.";
314
+ tasksEl.appendChild(empty);
315
+ } else {
316
+ tasks.forEach((task, index) => {
317
+ const item = document.createElement("li");
318
+ item.textContent = "Task " + (index + 1) + ": " + task.duration + " slots, complexity " + task.hidden_complexity;
319
+ tasksEl.appendChild(item);
320
+ });
321
+ }
322
+
323
+ payloadEl.textContent = JSON.stringify(response, null, 2);
324
+ }
325
+
326
+ async function callJson(url, body) {
327
+ const response = await fetch(url, {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/json" },
330
+ body: JSON.stringify(body),
331
+ });
332
+ const data = await response.json();
333
+ if (!response.ok) {
334
+ throw new Error(JSON.stringify(data));
335
+ }
336
+ return data;
337
+ }
338
+
339
+ async function resetEnv() {
340
+ setStatus("Resetting environment...");
341
+ try {
342
+ const data = await callJson("/reset", {});
343
+ renderObservation(data);
344
+ setStatus("Environment reset.");
345
+ } catch (error) {
346
+ setStatus("Reset failed: " + error.message, true);
347
+ }
348
+ }
349
+
350
+ async function stepEnv(operation) {
351
+ setStatus("Applying action...");
352
+ try {
353
+ const payload = {
354
+ action: {
355
+ target_slot: Number(targetSlotEl.value || 0),
356
+ operation: Number(operation),
357
+ },
358
+ };
359
+ const data = await callJson("/step", payload);
360
+ renderObservation(data);
361
+ setStatus("Action applied.");
362
+ } catch (error) {
363
+ setStatus("Action failed: " + error.message, true);
364
+ }
365
+ }
366
+
367
+ document.getElementById("resetBtn").addEventListener("click", resetEnv);
368
+ document.querySelectorAll("[data-op]").forEach((button) => {
369
+ button.addEventListener("click", () => stepEnv(button.dataset.op));
370
+ });
371
+
372
+ resetEnv();
373
+ </script>
374
+ </body>
375
+ </html>
376
+ """
377
+ )
378
+
379
+
380
+ @app.get("/", include_in_schema=False)
381
+ def root() -> RedirectResponse:
382
+ return RedirectResponse(url="/web/")
383
+
384
+
385
+ @app.get("/web", include_in_schema=False)
386
+ def web_root() -> RedirectResponse:
387
+ return RedirectResponse(url="/web/")
388
+
389
+
390
+ @app.get("/web/", include_in_schema=False)
391
+ def web_ui() -> HTMLResponse:
392
+ return HTMLResponse(WEB_PAGE)
393
+
394
 
395
  @app.get("/manifest.json", include_in_schema=False)
396
  def manifest() -> JSONResponse: