RunPodRun / Deployment_UI.py
Theflame47's picture
Update Deployment_UI.py
a42c87f verified
# 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)