File size: 13,433 Bytes
f0c9ba7
3334b2b
 
 
 
 
 
 
f0c9ba7
3334b2b
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3334b2b
 
f0c9ba7
 
 
 
 
 
 
3334b2b
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3334b2b
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
3334b2b
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3334b2b
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
a42c87f
f0c9ba7
 
 
a42c87f
 
 
3334b2b
a42c87f
f0c9ba7
 
a42c87f
3334b2b
a42c87f
f0c9ba7
 
 
3334b2b
a42c87f
f0c9ba7
 
 
 
 
 
3334b2b
a42c87f
f0c9ba7
a42c87f
f0c9ba7
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
a42c87f
f0c9ba7
 
 
 
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
a42c87f
f0c9ba7
 
 
 
 
 
 
 
 
 
 
3334b2b
a42c87f
f0c9ba7
 
 
a42c87f
f0c9ba7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3334b2b
0bbbc1e
f0c9ba7
 
 
 
 
 
3334b2b
f0c9ba7
3334b2b
f0c9ba7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# Deployment_UI.py — UI-only (binds to /api/* provided by Deployment_UI_BE.py)
from fastapi import APIRouter
from fastapi.responses import HTMLResponse

router = APIRouter()

@router.get("/Deployment_UI", response_class=HTMLResponse)
def deployment_ui_page():
    html_head = """
<!doctype html>
<html><head><meta charset="utf-8"/><title>Deployment UI</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 24px; display: flex; flex-direction: column; height: 90vh; }
  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; }
  a.button:hover { background:#eee; }
  #chat-window { flex:1; border:1px solid #ccc; border-radius:8px; padding:12px; overflow-y:auto; background:#fafafa; margin-bottom:10px; }
  #input-area { display:flex; gap:8px; margin-bottom:10px; }
  #user-input { flex:1; padding:10px; border:1px solid #ccc; border-radius:8px; }
  #send-btn { padding:10px 16px; border:none; border-radius:8px; background:#0078d7; color:#fff; cursor:pointer; }
  #send-btn:hover { background:#005fa3; }
  #control-bar { display:flex; gap:8px; margin:0 0 10px; }
  #start-btn, #stop-btn, #endall-btn { padding:8px 14px; border:1px solid #ccc; border-radius:8px; background:#f7f7f7; cursor:pointer; }
  #start-btn:hover, #stop-btn:hover, #endall-btn:hover { background:#eee; }
  #log-toggle { cursor:pointer; font-size:14px; color:#0078d7; text-decoration:underline; margin-bottom:6px; align-self:flex-start; }
  #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; }
  #logs.collapsed { max-height:0; padding:0; opacity:0; overflow:hidden; border:none; }
  .log-line { margin:0 0 6px; white-space:pre-wrap; }
  .log-raw { color:#333; } .log-ok { color:#2d7c2d; } .log-err { color:#9d1c1c; }
  #results { border:1px solid #ddd; border-radius:8px; padding:10px; background:#fff; margin-top:10px; }
  .result { margin:8px 0; }
  .thumb { max-width: 420px; border:1px solid #ccc; border-radius:6px; }
  .meta { font-size:12px; color:#555; margin-top:4px; }
  .download { display:inline-block; margin-top:6px; }
  /* Blob row (new, minimal) */
  #blob-bar { display:flex; gap:8px; margin:0 0 10px; align-items:center; }
  #blob-url { flex:1; padding:8px; border:1px solid #ccc; border-radius:8px; }
  #blob-load, #blob-ingest { padding:8px 12px; border:1px solid #ccc; border-radius:8px; background:#f7f7f7; cursor:pointer; }
  #blob-load:hover, #blob-ingest:hover { background:#eee; }
  #blob-status { font-size:12px; color:#555; }
  /* Blob preview box styled like logs/results */
  #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; }
  /* loading mask */
  #boot-mask{position:fixed;inset:0;background:rgba(255,255,255,.85);display:none;
    align-items:center;justify-content:center;flex-direction:column;z-index:9999}
  #boot-msg{margin-top:12px;color:#000;font-weight:600}
  .spinner{width:36px;height:36px;border:3px solid #ddd;border-top-color:#0078d7;border-radius:50%;
    animation:spin 1s linear infinite}
  @keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
  <a href="/trythis" class="button">← Back</a>
  <!-- New: Blob tools row (keeps your visual rhythm) -->
  <div id="blob-bar">
    <input id="blob-url" type="text" placeholder="/modelblob.json" />
    <button id="blob-load">Load Blob</button>
    <button id="blob-ingest" disabled>Ingest → BE</button>
    <div id="blob-status"></div>
  </div>
  <pre id="blob-preview" aria-live="polite" style="display:none;"></pre>
  <div id="chat-window"></div>
  <div id="input-area">
    <input type="text" id="user-input" placeholder="Describe an image to generate…" />
    <button id="send-btn">Send</button>
  </div>
  <div id="control-bar">
    <button id="start-btn">Create inst.</button>
    <button id="stop-btn">Stop</button>
    <button id="endall-btn">End All</button>
  </div>
  <div id="log-toggle">Hide Logs</div>
  <div id="logs" aria-live="polite"></div>
  <div id="results"></div>
  <div id="boot-mask" aria-live="polite" role="status">
    <div class="spinner"></div>
    <div id="boot-msg">Starting…</div>
  </div>
<script>
"""
    html_tail = """
  const logs = document.getElementById('logs');
  const toggleBtn = document.getElementById('log-toggle');
  const startBtn = document.getElementById('start-btn');
  const stopBtn = document.getElementById('stop-btn');
  const endAllBtn = document.getElementById('endall-btn');
  const sendBtn = document.getElementById('send-btn');
  const userInput = document.getElementById('user-input');
  const results = document.getElementById('results');
  const chat = document.getElementById('chat-window');
  const bootMask = document.getElementById('boot-mask');
  const bootMsg  = document.getElementById('boot-msg');
  // New blob controls
  const blobUrl = document.getElementById('blob-url');
  const blobLoad = document.getElementById('blob-load');
  const blobIngest = document.getElementById('blob-ingest');
  const blobStatus = document.getElementById('blob-status');
  const blobPreview = document.getElementById('blob-preview');
  let running = false;
  let MODEL_BASE = null, PREDICT_ROUTE = null;

  // Maintain your existing auto-ingest-on-load behavior (unchanged)
  (async () => {
    try {
      const u = new URL(window.location.href);
      const url = u.searchParams.get('blob_url') || '/modelblob.json';
      const r = await fetch(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: 'POST' });
      const t = await r.text();
      console.log('ingest-from-landing', r.status, t.slice(0,200));
    } catch (e) {
      console.warn('ingest bootstrap failed:', e);
    }
  })();

  function showBoot(msg){ bootMsg.textContent = msg || 'Starting…'; bootMask.style.display = 'flex'; }
  function setBoot(msg){  bootMsg.textContent = msg || 'Working…'; }
  function hideBoot(){    bootMask.style.display = 'none'; }

  function log(msg, cls='log-raw') {
    const line = document.createElement('div');
    line.className = 'log-line ' + cls;
    line.textContent = String(msg);
    logs.appendChild(line);
    logs.scrollTop = logs.scrollHeight;
  }

  toggleBtn.addEventListener('click', () => {
    const collapsed = logs.classList.toggle('collapsed');
    toggleBtn.textContent = collapsed ? "Show Logs" : "Hide Logs";
  });

  async function post(path, payload) {
    const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload || {}) });
    const text = await r.text();
    log(text, r.ok ? 'log-ok' : 'log-err');
    try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
  }

  async function del(path) {
    const r = await fetch(path, { method: 'DELETE' });
    const text = await r.text();
    log(text, r.ok ? 'log-ok' : 'log-err');
    try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
  }

  // New: explicit blob load/preview + ingest flow (mirrors landing page proof)
  async function loadBlob() {
    const url = (blobUrl.value || '/modelblob.json').trim();
    blobStatus.textContent = 'Loading…';
    try {
      const r = await fetch(url, { method: 'GET' });
      const ct = (r.headers.get('content-type') || '').toLowerCase();
      if (!ct.includes('application/json')) {
        const raw = await r.text();
        throw new Error(`Non-JSON ${r.status}: ${raw.slice(0,200)}`);
      }
      const j = await r.json();
      blobPreview.style.display = 'block';
      blobPreview.textContent = JSON.stringify(j, null, 2);
      blobStatus.textContent = 'Blob loaded';
      blobIngest.disabled = false;
      log(`BLOB_LOADED from ${url}`, 'log-ok');
    } catch (e) {
      blobPreview.style.display = 'block';
      blobPreview.textContent = String(e.message || e);
      blobStatus.textContent = 'Load failed';
      blobIngest.disabled = true;
      log(`BLOB_LOAD_ERROR ${e.message || e}`, 'log-err');
    }
  }

  async function ingestBlob() {
    const url = (blobUrl.value || '/modelblob.json').trim();
    blobStatus.textContent = 'Ingesting…';
    try {
      const r = await fetch(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: 'POST' });
      const t = await r.text();
      log(`BLOB_INGEST ${r.status} ${t.slice(0,160)}`, r.ok ? 'log-ok' : 'log-err');
      blobStatus.textContent = r.ok ? 'Ingested' : 'Ingest failed';
    } catch (e) {
      blobStatus.textContent = 'Ingest failed';
      log(`BLOB_INGEST_ERROR ${e.message || e}`, 'log-err');
    }
  }

  blobLoad.addEventListener('click', loadBlob);
  blobIngest.addEventListener('click', ingestBlob);

  // create instance (no readiness gating)
  async function begin() {
    if (running) return;
    running = true;
    showBoot('Creating instance…');
    const create = await post('/api/compute/create_instance');
    hideBoot();
    if (!create.ok) {
      running = false;
    }
  }

  async function stopOnce() {
    await del('/api/compute/delete_instance');
    hideBoot(); setBoot(''); running = false;
  }

  async function endAll() {
    await stopOnce();
    MODEL_BASE = null; PREDICT_ROUTE = null;
  }

  function appendResult(job_id, b64, timings) {
    const div = document.createElement('div'); div.className = 'result';
    const img = document.createElement('img'); img.className = 'thumb'; img.src = 'data:image/png;base64,' + b64;
    const a = document.createElement('a'); a.className = 'download'; a.textContent = 'Download'; a.href = img.src; a.download = `job_${job_id}.png`;
    const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `job ${job_id} | ${JSON.stringify(timings || {})}`;
    div.append(img, document.createTextNode(' '), a, meta); results.prepend(div);
  }

  function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }

  function addBlock(sender, html) {
    const wrap = document.createElement('div');
    wrap.style.margin = '8px 0';
    wrap.innerHTML = `<div class="meta"><strong>${sender}:</strong></div><div>${html}</div>`;
    chat.appendChild(wrap);
    chat.scrollTop = chat.scrollHeight;
    return wrap;
  }

  function addUser(text)   { return addBlock('You',  `<div>${escapeHtml(text)}</div>`); }
  function addModel(text)  { return addBlock('Model', `<div>${escapeHtml(text)}</div>`); }

  function addModelImg(b64){
    const wrap = addBlock('Model', '');
    const img = document.createElement('img');
    img.className = 'thumb';
    img.src = 'data:image/png;base64,' + b64;
    wrap.lastElementChild.appendChild(img);
    return wrap;
  }

  function addLoader() {
    const wrap = addBlock('Model', `<div id="spinner" style="display:inline-block">Loading…</div>`);
    return wrap;
  }

  function looksBase64(s){ return /^[A-Za-z0-9+/=\\s]+$/.test(s||'') && String(s||'').length > 100; }

  // backend hop for prompts
  async function sendViaBackend(prompt) {
    const r = await fetch('/api/middleware/infer', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ prompt })
    });
    const text = await r.text();
    log(`POST /api/middleware/infer → ${r.status}`, r.ok ? 'log-ok' : 'log-err');
    try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
  }

  async function sendMessage() {
    const prompt = (userInput.value || '').trim();
    if (!prompt) return;

    addUser(prompt);
    const loader = addLoader();
    userInput.disabled = true;
    try {
      const { ok, json } = await sendViaBackend(prompt);
      loader.remove();
      if (!ok && json?.error) { addModel(`Error: ${json.error}`); return; }
      if (json?.image_b64) {
        addModelImg(json.image_b64);
        if (json.timings) appendResult(String(Date.now()), json.image_b64, json.timings);
      } else if (typeof json?.output === 'string' && looksBase64(json.output)) {
        addModelImg(json.output);
      } else if (typeof json?.output === 'string') {
        addModel(json.output);
      } else if (json?._raw) {
        addModel(json._raw);
      } else {
        addModel(JSON.stringify(json || {}, null, 2));
      }
    } catch (e) {
      loader.remove();
      addModel(`Error: ${String(e)}`);
    } finally {
      userInput.disabled = false; userInput.value = '';
      userInput.focus();
    }
  }

  document.getElementById('send-btn').addEventListener('click', sendMessage);
  document.getElementById('user-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } });
  document.getElementById('start-btn').addEventListener('click', begin);
  document.getElementById('stop-btn').addEventListener('click', stopOnce);
  document.getElementById('endall-btn').addEventListener('click', endAll);
  (function init(){})();
</script>
</body></html>
"""
    return HTMLResponse(content=html_head + html_tail)