Theflame47 commited on
Commit
f0c9ba7
·
verified ·
1 Parent(s): 0bbbc1e

Update Deployment_UI.py

Browse files
Files changed (1) hide show
  1. Deployment_UI.py +295 -225
Deployment_UI.py CHANGED
@@ -1,4 +1,4 @@
1
- # Deployment_UI.py — FE page with explicit Blob load/preview + ingest
2
  from fastapi import APIRouter
3
  from fastapi.responses import HTMLResponse
4
 
@@ -6,245 +6,315 @@ router = APIRouter()
6
 
7
  @router.get("/Deployment_UI", response_class=HTMLResponse)
8
  def deployment_ui_page():
9
- html = """
10
  <!doctype html>
11
- <html lang="en">
12
- <head>
13
- <meta charset="utf-8"/>
14
- <title>Deployment UI</title>
15
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
16
- <style>
17
- :root { --gap: 12px; --pad: 12px; --border: 1px solid #ddd; --radius: 10px; }
18
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 20px; color: #111; }
19
- h1 { margin: 0 0 6px; font-size: 20px; }
20
- .row { display: flex; gap: var(--gap); flex-wrap: wrap; }
21
- .col { flex: 1 1 360px; min-width: 320px; }
22
- .card { border: var(--border); border-radius: var(--radius); padding: var(--pad); background: #fff; }
23
- .btn { border: 1px solid #ccc; background: #f7f7f7; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
24
- .btn:hover { background: #eee; }
25
- .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
26
- input[type="text"] { width: 100%; padding: 8px; border: var(--border); border-radius: 8px; }
27
- pre { white-space: pre-wrap; word-break: break-word; background: #fafafa; border: var(--border); border-radius: 8px; padding: 8px; max-height: 360px; overflow: auto; }
28
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
29
- .muted { color: #666; }
30
- .ok { color: #0a7f2e; }
31
- .err { color: #9b1c1c; }
32
- .kvs { font-size: 13px; }
33
- .line { display:flex; gap:8px; align-items:center; margin: 6px 0; }
34
- .small { font-size: 12px; }
35
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  </head>
37
  <body>
38
- <h1>Deployment UI</h1>
39
- <div class="row">
40
- <div class="col">
41
- <div class="card">
42
- <div class="line">
43
- <label for="blobUrl"><strong>Blob URL</strong></label>
44
- <span class="muted small">(defaults to <code class="mono">/modelblob.json</code>)</span>
45
- </div>
46
- <input id="blobUrl" type="text" placeholder="/modelblob.json"/>
47
- <div class="line">
48
- <button id="btnLoadBlob" class="btn">Load Blob</button>
49
- <button id="btnIngest" class="btn" disabled>Ingest → BE</button>
50
- <span id="blobStatus" class="small muted"></span>
51
- </div>
52
- <pre id="blobPreview" class="mono muted">Blob not loaded.</pre>
53
- </div>
54
- </div>
55
-
56
- <div class="col">
57
- <div class="card">
58
- <div class="line"><strong>Actions</strong></div>
59
- <div class="line">
60
- <button id="btnCreate" class="btn" disabled>Create Instance</button>
61
- <button id="btnStart" class="btn" disabled>Start</button>
62
- <button id="btnWait" class="btn" disabled>Wait</button>
63
- </div>
64
- <div class="line">
65
- <button id="btnStop" class="btn" disabled>Stop</button>
66
- <button id="btnEndAll" class="btn" disabled>End All</button>
67
- </div>
68
- <div class="kvs mono" id="stateKvs"></div>
69
- </div>
70
- </div>
71
- </div>
72
 
73
- <div class="row" style="margin-top:12px;">
74
- <div class="col">
75
- <div class="card">
76
- <div class="line"><strong>Logs</strong> <span class="muted small">(key responses)</span></div>
77
- <pre id="logs" class="mono"></pre>
78
- </div>
79
- </div>
80
  </div>
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  <script>
83
- const $ = (id) => document.getElementById(id);
84
- const logsEl = $("logs");
85
- const blobPreviewEl = $("blobPreview");
86
- const blobStatusEl = $("blobStatus");
87
- const stateKvsEl = $("stateKvs");
88
-
89
- let cachedBlob = null;
90
- let cachedPodId = null;
91
-
92
- function logLine(s, cls="") {
93
- const at = new Date().toLocaleTimeString();
94
- const line = `[${at}] ${s}\\n`;
95
- logsEl.textContent += line;
96
- logsEl.scrollTop = logsEl.scrollHeight;
97
- }
98
-
99
- function setActionsEnabled(enabled) {
100
- $("btnCreate").disabled = !enabled;
101
- $("btnStart").disabled = !enabled || !cachedPodId;
102
- $("btnWait").disabled = !enabled || !cachedPodId;
103
- $("btnStop").disabled = !enabled || !cachedPodId;
104
- $("btnEndAll").disabled = !enabled || !cachedPodId;
105
- }
106
-
107
- function updateStateKvs(obj) {
108
- const st = obj || {};
109
- const cs = (st.cachedState || {});
110
- const kvs = [
111
- ["podId", cs.podId || ""],
112
- ["status", cs.status || ""],
113
- ["ip", cs.ip || ""],
114
- ["port", cs.port || ""],
115
- ["predictRoute", cs.predictRoute || ""],
116
- ["base", cs.base || ""],
117
- ];
118
- stateKvsEl.innerHTML = kvs.map(([k,v]) => `<div><strong>${k}</strong>: <span class="mono">${String(v)}</span></div>`).join("");
119
- if (cs.podId) cachedPodId = cs.podId;
120
- setActionsEnabled(true);
121
- }
122
-
123
- async function fetchJSON(url, opts={}) {
124
- const r = await fetch(url, opts);
125
- const ct = r.headers.get("content-type") || "";
126
- if (!ct.includes("application/json")) {
127
- const t = await r.text();
128
- throw new Error(`Non-JSON response ${r.status}: ${t.slice(0,200)}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
- const j = await r.json();
131
- if (!r.ok) throw new Error(JSON.stringify(j).slice(0,200));
132
- return j;
133
- }
134
-
135
- function pretty(obj) {
136
- try { return JSON.stringify(obj, null, 2); } catch { return String(obj); }
137
- }
138
-
139
- $("btnLoadBlob").addEventListener("click", async () => {
140
- const url = ($("blobUrl").value || "/modelblob.json").trim();
141
- blobStatusEl.textContent = "Loading…";
142
- try {
143
- const j = await fetchJSON(url, { method: "GET" });
144
- cachedBlob = j;
145
- blobPreviewEl.textContent = pretty(j);
146
- blobPreviewEl.classList.remove("muted");
147
- blobStatusEl.textContent = "Blob loaded";
148
- $("btnIngest").disabled = false;
149
- logLine(`BLOB_LOADED from ${url}`);
150
- } catch (e) {
151
- blobStatusEl.textContent = "Load failed";
152
- blobPreviewEl.textContent = String(e.message || e);
153
- blobPreviewEl.classList.remove("muted");
154
- $("btnIngest").disabled = true;
155
- logLine(`BLOB_LOAD_ERROR ${e.message || e}`, "err");
156
  }
157
- });
158
-
159
- $("btnIngest").addEventListener("click", async () => {
160
- const url = ($("blobUrl").value || "/modelblob.json").trim();
161
- blobStatusEl.textContent = "Ingesting…";
162
- try {
163
- const j = await fetchJSON(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: "POST" });
164
- blobStatusEl.textContent = `Ingested (${j.source || url})`;
165
- $("btnCreate").disabled = false;
166
- logLine(`BLOB_INGEST_OK source=${j.source || url}`);
167
- } catch (e) {
168
- blobStatusEl.textContent = "Ingest failed";
169
- logLine(`BLOB_INGEST_ERROR ${e.message || e}`, "err");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
- });
172
-
173
- $("btnCreate").addEventListener("click", async () => {
174
- setActionsEnabled(false);
175
- logLine("CREATE → /api/compute/create_instance");
176
- try {
177
- const j = await fetchJSON("/api/compute/create_instance", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
178
- logLine("CREATE_OK " + (j.id || j.podId || JSON.stringify(j).slice(0,120)));
179
- await refreshStatus();
180
- } catch (e) {
181
- logLine("CREATE_ERROR " + (e.message || e), "err");
182
- } finally {
183
- setActionsEnabled(true);
184
  }
185
- });
186
-
187
- $("btnStart").addEventListener("click", async () => {
188
- if (!cachedPodId) return;
189
- setActionsEnabled(false);
190
- logLine(`START → /api/compute/pods/${cachedPodId}/start`);
191
- try {
192
- const j = await fetchJSON(`/api/compute/pods/${cachedPodId}/start`, { method: "POST" });
193
- logLine("START_OK " + JSON.stringify(j).slice(0,120));
194
- } catch (e) {
195
- logLine("START_ERROR " + (e.message || e), "err");
196
- } finally {
197
- setActionsEnabled(true);
198
  }
199
- });
200
-
201
- $("btnWait").addEventListener("click", async () => {
202
- await refreshStatus(true);
203
- });
204
-
205
- $("btnStop").addEventListener("click", async () => {
206
- setActionsEnabled(false);
207
- logLine("STOP → /api/compute/delete_instance");
208
- try {
209
- const j = await fetchJSON("/api/compute/delete_instance", { method: "DELETE" });
210
- logLine("STOP_OK " + JSON.stringify(j).slice(0,120));
211
- } catch (e) {
212
- logLine("STOP_ERROR " + (e.message || e), "err");
213
- } finally {
214
- await refreshStatus();
215
- setActionsEnabled(true);
216
  }
217
- });
218
-
219
- $("btnEndAll").addEventListener("click", async () => {
220
- if (!cachedPodId) return;
221
- setActionsEnabled(false);
222
- logLine(`END_ALL → /api/compute/end_all`);
223
- try {
224
- const j = await fetchJSON("/api/compute/end_all", { method: "DELETE" });
225
- logLine("END_ALL_OK " + JSON.stringify(j).slice(0,120));
226
- } catch (e) {
227
- logLine("END_ALL_ERROR " + (e.message || e), "err");
228
- } finally {
229
- await refreshStatus();
230
- setActionsEnabled(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
- });
233
-
234
- async function refreshStatus(verbose=false) {
235
- try {
236
- const j = await fetchJSON("/api/compute/pods/placeholder?pod_id=", { method: "GET" });
237
- if (verbose) logLine("STATUS " + JSON.stringify(j).slice(0,180));
238
- updateStateKvs(j);
239
- } catch (e) {
240
- logLine("STATUS_ERROR " + (e.message || e), "err");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
- }
243
 
244
- // On load: try to show current state (if any); do NOT auto-ingest.
245
- refreshStatus();
 
 
 
 
246
  </script>
247
- </body>
248
- </html>
249
  """
250
- return HTMLResponse(html)
 
1
+ # Deployment_UI.py — UI-only (binds to /api/* provided by Deployment_UI_BE.py)
2
  from fastapi import APIRouter
3
  from fastapi.responses import HTMLResponse
4
 
 
6
 
7
  @router.get("/Deployment_UI", response_class=HTMLResponse)
8
  def deployment_ui_page():
9
+ html_head = """
10
  <!doctype html>
11
+ <html><head><meta charset="utf-8"/><title>Deployment UI</title>
12
+ <style>
13
+ body { font-family: system-ui, sans-serif; margin: 24px; display: flex; flex-direction: column; height: 90vh; }
14
+ a.button { display:inline-block; padding:8px 14px; margin-bottom:10px; border:1px solid #ccc; border-radius:6px; text-decoration:none; color:#000; background:#f7f7f7; }
15
+ a.button:hover { background:#eee; }
16
+ #chat-window { flex:1; border:1px solid #ccc; border-radius:8px; padding:12px; overflow-y:auto; background:#fafafa; margin-bottom:10px; }
17
+ #input-area { display:flex; gap:8px; margin-bottom:10px; }
18
+ #user-input { flex:1; padding:10px; border:1px solid #ccc; border-radius:8px; }
19
+ #send-btn { padding:10px 16px; border:none; border-radius:8px; background:#0078d7; color:#fff; cursor:pointer; }
20
+ #send-btn:hover { background:#005fa3; }
21
+ #control-bar { display:flex; gap:8px; margin:0 0 10px; }
22
+ #start-btn, #stop-btn, #endall-btn { padding:8px 14px; border:1px solid #ccc; border-radius:8px; background:#f7f7f7; cursor:pointer; }
23
+ #start-btn:hover, #stop-btn:hover, #endall-btn:hover { background:#eee; }
24
+ #log-toggle { cursor:pointer; font-size:14px; color:#0078d7; text-decoration:underline; margin-bottom:6px; align-self:flex-start; }
25
+ #logs { border:1px dashed #bbb; border-radius:8px; padding:10px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; background:#fff; max-height:220px; overflow-y:auto; transition:max-height .25s ease, opacity .25s ease; opacity:1; }
26
+ #logs.collapsed { max-height:0; padding:0; opacity:0; overflow:hidden; border:none; }
27
+ .log-line { margin:0 0 6px; white-space:pre-wrap; }
28
+ .log-raw { color:#333; } .log-ok { color:#2d7c2d; } .log-err { color:#9d1c1c; }
29
+ #results { border:1px solid #ddd; border-radius:8px; padding:10px; background:#fff; margin-top:10px; }
30
+ .result { margin:8px 0; }
31
+ .thumb { max-width: 420px; border:1px solid #ccc; border-radius:6px; }
32
+ .meta { font-size:12px; color:#555; margin-top:4px; }
33
+ .download { display:inline-block; margin-top:6px; }
34
+
35
+ /* Blob row (new, minimal) */
36
+ #blob-bar { display:flex; gap:8px; margin:0 0 10px; align-items:center; }
37
+ #blob-url { flex:1; padding:8px; border:1px solid #ccc; border-radius:8px; }
38
+ #blob-load, #blob-ingest { padding:8px 12px; border:1px solid #ccc; border-radius:8px; background:#f7f7f7; cursor:pointer; }
39
+ #blob-load:hover, #blob-ingest:hover { background:#eee; }
40
+ #blob-status { font-size:12px; color:#555; }
41
+
42
+ /* Blob preview box styled like logs/results */
43
+ #blob-preview { border:1px solid #ddd; border-radius:8px; padding:10px; background:#fff; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; max-height:220px; overflow:auto; margin-bottom:10px; }
44
+
45
+ /* loading mask */
46
+ #boot-mask{position:fixed;inset:0;background:rgba(255,255,255,.85);display:none;
47
+ align-items:center;justify-content:center;flex-direction:column;z-index:9999}
48
+ #boot-msg{margin-top:12px;color:#000;font-weight:600}
49
+ .spinner{width:36px;height:36px;border:3px solid #ddd;border-top-color:#0078d7;border-radius:50%;
50
+ animation:spin 1s linear infinite}
51
+ @keyframes spin{to{transform:rotate(360deg)}}
52
+ </style>
53
  </head>
54
  <body>
55
+ <a href="/trythis" class="button">← Back</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ <!-- New: Blob tools row (keeps your visual rhythm) -->
58
+ <div id="blob-bar">
59
+ <input id="blob-url" type="text" placeholder="/modelblob.json" />
60
+ <button id="blob-load">Load Blob</button>
61
+ <button id="blob-ingest" disabled>Ingest → BE</button>
62
+ <div id="blob-status"></div>
 
63
  </div>
64
+ <pre id="blob-preview" aria-live="polite" style="display:none;"></pre>
65
 
66
+ <div id="chat-window"></div>
67
+ <div id="input-area">
68
+ <input type="text" id="user-input" placeholder="Describe an image to generate…" />
69
+ <button id="send-btn">Send</button>
70
+ </div>
71
+ <div id="control-bar">
72
+ <button id="start-btn">Create inst.</button>
73
+ <button id="stop-btn">Stop</button>
74
+ <button id="endall-btn">End All</button>
75
+ </div>
76
+ <div id="log-toggle">Hide Logs</div>
77
+ <div id="logs" aria-live="polite"></div>
78
+ <div id="results"></div>
79
+ <div id="boot-mask" aria-live="polite" role="status">
80
+ <div class="spinner"></div>
81
+ <div id="boot-msg">Starting…</div>
82
+ </div>
83
  <script>
84
+ """
85
+ html_tail = """
86
+ const logs = document.getElementById('logs');
87
+ const toggleBtn = document.getElementById('log-toggle');
88
+ const startBtn = document.getElementById('start-btn');
89
+ const stopBtn = document.getElementById('stop-btn');
90
+ const endAllBtn = document.getElementById('endall-btn');
91
+ const sendBtn = document.getElementById('send-btn');
92
+ const userInput = document.getElementById('user-input');
93
+ const results = document.getElementById('results');
94
+ const chat = document.getElementById('chat-window');
95
+ const bootMask = document.getElementById('boot-mask');
96
+ const bootMsg = document.getElementById('boot-msg');
97
+
98
+ // New blob controls
99
+ const blobUrl = document.getElementById('blob-url');
100
+ const blobLoad = document.getElementById('blob-load');
101
+ const blobIngest = document.getElementById('blob-ingest');
102
+ const blobStatus = document.getElementById('blob-status');
103
+ const blobPreview = document.getElementById('blob-preview');
104
+
105
+ let running = false, ready = false;
106
+ let MODEL_BASE = null, PREDICT_ROUTE = null;
107
+
108
+ // Maintain your existing auto-ingest-on-load behavior (unchanged)
109
+ (async () => {
110
+ try {
111
+ const u = new URL(window.location.href);
112
+ const url = u.searchParams.get('blob_url') || '/modelblob.json';
113
+ const r = await fetch(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: 'POST' });
114
+ const t = await r.text();
115
+ console.log('ingest-from-landing', r.status, t.slice(0,200));
116
+ } catch (e) {
117
+ console.warn('ingest bootstrap failed:', e);
118
+ }
119
+ })();
120
+
121
+ function showBoot(msg){ bootMsg.textContent = msg || 'Starting…'; bootMask.style.display = 'flex'; }
122
+ function setBoot(msg){ bootMsg.textContent = msg || 'Working…'; }
123
+ function hideBoot(){ bootMask.style.display = 'none'; }
124
+ function log(msg, cls='log-raw') {
125
+ const line = document.createElement('div');
126
+ line.className = 'log-line ' + cls;
127
+ line.textContent = String(msg);
128
+ logs.appendChild(line);
129
+ logs.scrollTop = logs.scrollHeight;
130
+ }
131
+ toggleBtn.addEventListener('click', () => {
132
+ const collapsed = logs.classList.toggle('collapsed');
133
+ toggleBtn.textContent = collapsed ? "Show Logs" : "Hide Logs";
134
+ });
135
+
136
+ async function post(path, payload) {
137
+ const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload || {}) });
138
+ const text = await r.text();
139
+ log(text, r.ok ? 'log-ok' : 'log-err');
140
+ try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
141
+ }
142
+ async function del(path) {
143
+ const r = await fetch(path, { method: 'DELETE' });
144
+ const text = await r.text();
145
+ log(text, r.ok ? 'log-ok' : 'log-err');
146
+ try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
147
  }
148
+
149
+ // New: explicit blob load/preview + ingest flow (mirrors landing page proof)
150
+ async function loadBlob() {
151
+ const url = (blobUrl.value || '/modelblob.json').trim();
152
+ blobStatus.textContent = 'Loading…';
153
+ try {
154
+ const r = await fetch(url, { method: 'GET' });
155
+ const ct = (r.headers.get('content-type') || '').toLowerCase();
156
+ if (!ct.includes('application/json')) {
157
+ const raw = await r.text();
158
+ throw new Error(`Non-JSON ${r.status}: ${raw.slice(0,200)}`);
159
+ }
160
+ const j = await r.json();
161
+ blobPreview.style.display = 'block';
162
+ blobPreview.textContent = JSON.stringify(j, null, 2);
163
+ blobStatus.textContent = 'Blob loaded';
164
+ blobIngest.disabled = false;
165
+ log(`BLOB_LOADED from ${url}`, 'log-ok');
166
+ } catch (e) {
167
+ blobPreview.style.display = 'block';
168
+ blobPreview.textContent = String(e.message || e);
169
+ blobStatus.textContent = 'Load failed';
170
+ blobIngest.disabled = true;
171
+ log(`BLOB_LOAD_ERROR ${e.message || e}`, 'log-err');
172
+ }
 
173
  }
174
+
175
+ async function ingestBlob() {
176
+ const url = (blobUrl.value || '/modelblob.json').trim();
177
+ blobStatus.textContent = 'Ingesting…';
178
+ try {
179
+ const r = await fetch(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: 'POST' });
180
+ const t = await r.text();
181
+ log(`BLOB_INGEST ${r.status} ${t.slice(0,160)}`, r.ok ? 'log-ok' : 'log-err');
182
+ blobStatus.textContent = r.ok ? 'Ingested' : 'Ingest failed';
183
+ } catch (e) {
184
+ blobStatus.textContent = 'Ingest failed';
185
+ log(`BLOB_INGEST_ERROR ${e.message || e}`, 'log-err');
186
+ }
187
+ }
188
+
189
+ blobLoad.addEventListener('click', loadBlob);
190
+ blobIngest.addEventListener('click', ingestBlob);
191
+
192
+ // create + wait for true readiness (isReady)
193
+ async function begin() {
194
+ if (running) return;
195
+ running = true; ready = false;
196
+ showBoot('Creating instance…');
197
+ const create = await post('/api/compute/create_instance');
198
+ if (!create.ok) { running = false; hideBoot(); return; }
199
+ const ok = await ensureReady(true);
200
+ hideBoot();
201
  }
202
+ async function stopOnce() {
203
+ await del('/api/compute/delete_instance');
204
+ hideBoot(); setBoot(''); ready = false; running = false;
 
 
 
 
 
 
 
 
 
 
205
  }
206
+ async function endAll() {
207
+ await stopOnce();
208
+ MODEL_BASE = null; PREDICT_ROUTE = null;
 
 
 
 
 
 
 
 
 
 
209
  }
210
+ function appendResult(job_id, b64, timings) {
211
+ const div = document.createElement('div'); div.className = 'result';
212
+ const img = document.createElement('img'); img.className = 'thumb'; img.src = 'data:image/png;base64,' + b64;
213
+ const a = document.createElement('a'); a.className = 'download'; a.textContent = 'Download'; a.href = img.src; a.download = `job_${job_id}.png`;
214
+ const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `job ${job_id} | ${JSON.stringify(timings || {})}`;
215
+ div.append(img, document.createTextNode(' '), a, meta); results.prepend(div);
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
+ function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
218
+ function addBlock(sender, html) {
219
+ const wrap = document.createElement('div');
220
+ wrap.style.margin = '8px 0';
221
+ wrap.innerHTML = `<div class="meta"><strong>${sender}:</strong></div><div>${html}</div>`;
222
+ chat.appendChild(wrap);
223
+ chat.scrollTop = chat.scrollHeight;
224
+ return wrap;
225
+ }
226
+ function addUser(text) { return addBlock('You', `<div>${escapeHtml(text)}</div>`); }
227
+ function addModel(text) { return addBlock('Model', `<div>${escapeHtml(text)}</div>`); }
228
+ function addModelImg(b64){
229
+ const wrap = addBlock('Model', '');
230
+ const img = document.createElement('img');
231
+ img.className = 'thumb';
232
+ img.src = 'data:image/png;base64,' + b64;
233
+ wrap.lastElementChild.appendChild(img);
234
+ return wrap;
235
+ }
236
+ function addLoader() {
237
+ const wrap = addBlock('Model', `<div id="spinner" style="display:inline-block">Loading…</div>`);
238
+ return wrap;
239
+ }
240
+ function looksBase64(s){ return /^[A-Za-z0-9+/=\\s]+$/.test(s||'') && String(s||'').length > 100; }
241
+
242
+ // poll for true readiness (kept identical to your flow)
243
+ async function ensureReady(verbose=false) {
244
+ for (let i = 0; i < 60; i++) {
245
+ const r = await fetch('/api/compute/wait_instance');
246
+ const j = await r.json();
247
+ const cs = j.cachedState || {};
248
+ const status = (cs.status || '').toUpperCase();
249
+ if (verbose) {
250
+ if (cs.isReady === true) setBoot('Ready');
251
+ else if (status === 'RUNNING' && cs.base) setBoot('Warming model…');
252
+ else setBoot(`Status: ${status || '…'}`);
253
+ }
254
+ if (cs.base && cs.predictRoute && cs.isReady === true) {
255
+ MODEL_BASE = cs.base;
256
+ PREDICT_ROUTE = cs.predictRoute.startsWith('/') ? cs.predictRoute : `/${cs.predictRoute}`;
257
+ log(`PROMPT_ENDPOINT ${MODEL_BASE}${PREDICT_ROUTE}`, 'log-ok');
258
+ ready = true;
259
+ return true;
260
+ }
261
+ log(`READY_POLL base=${cs.base ? 'yes' : 'no'} route=${cs.predictRoute || ''} isReady=${cs.isReady === true} status=${status}`, 'log-raw');
262
+ await new Promise(res => setTimeout(res, 1000));
263
+ }
264
+ ready = false;
265
+ return false;
266
+ }
267
+
268
+ // backend hop for prompts
269
+ async function sendViaBackend(prompt) {
270
+ const r = await fetch('/api/middleware/infer', {
271
+ method: 'POST',
272
+ headers: { 'content-type': 'application/json' },
273
+ body: JSON.stringify({ prompt })
274
+ });
275
+ const text = await r.text();
276
+ log(`POST /api/middleware/infer → ${r.status}`, r.ok ? 'log-ok' : 'log-err');
277
+ try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
278
  }
279
+ async function sendMessage() {
280
+ const prompt = (userInput.value || '').trim();
281
+ if (!prompt) return;
282
+ if (!ready) { addModel('Instance not ready yet. Try Start, or wait a moment.'); return; }
283
+ addUser(prompt);
284
+ const loader = addLoader();
285
+ userInput.disabled = true;
286
+ try {
287
+ const { ok, json } = await sendViaBackend(prompt);
288
+ loader.remove();
289
+ if (!ok && json?.error) { addModel(`Error: ${json.error}`); return; }
290
+ if (json?.image_b64) {
291
+ addModelImg(json.image_b64);
292
+ if (json.timings) appendResult(String(Date.now()), json.image_b64, json.timings);
293
+ } else if (typeof json?.output === 'string' && looksBase64(json.output)) {
294
+ addModelImg(json.output);
295
+ } else if (typeof json?.output === 'string') {
296
+ addModel(json.output);
297
+ } else if (json?._raw) {
298
+ addModel(json._raw);
299
+ } else {
300
+ addModel(JSON.stringify(json || {}, null, 2));
301
+ }
302
+ } catch (e) {
303
+ loader.remove();
304
+ addModel(`Error: ${String(e)}`);
305
+ } finally {
306
+ userInput.disabled = false; userInput.value = '';
307
+ userInput.focus();
308
+ }
309
  }
 
310
 
311
+ document.getElementById('send-btn').addEventListener('click', sendMessage);
312
+ document.getElementById('user-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } });
313
+ document.getElementById('start-btn').addEventListener('click', begin);
314
+ document.getElementById('stop-btn').addEventListener('click', stopOnce);
315
+ document.getElementById('endall-btn').addEventListener('click', endAll);
316
+ (function init(){})();
317
  </script>
318
+ </body></html>
 
319
  """
320
+ return HTMLResponse(content=html_head + html_tail)