Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions inferedgelab/studio/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ function setEmpty(selector, label = "No data available") {
}

async function fetchJson(url) {
assertHttpStudio();
const response = await fetch(url, { headers: { Accept: "application/json" } });
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
throw new Error(await responseErrorMessage(response));
}
return response.json();
}

async function postJson(url, payload) {
assertHttpStudio();
const response = await fetch(url, {
method: "POST",
headers: {
Expand All @@ -92,12 +94,34 @@ async function postJson(url, payload) {
body: JSON.stringify(payload),
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Request failed: ${response.status}`);
throw new Error(await responseErrorMessage(response));
}
return response.json();
}

function assertHttpStudio() {
if (window.location.protocol === "file:") {
throw new Error("Open Studio from http://127.0.0.1:8000/studio so it can call the local API.");
}
}

async function responseErrorMessage(response) {
const fallback = `Request failed: ${response.status}`;
try {
const payload = await response.clone().json();
if (payload?.detail) {
return Array.isArray(payload.detail) ? payload.detail.map(String).join("; ") : String(payload.detail);
}
if (payload?.error) {
return typeof payload.error === "string" ? payload.error : JSON.stringify(payload.error);
}
} catch (error) {
const text = await response.text();
return text || fallback;
}
return fallback;
}

async function loadJobs(preferredJobId = selectedJobId) {
setLoading("#job-list");
try {
Expand Down Expand Up @@ -191,7 +215,7 @@ async function runModel() {
setState("#run-state", "completed");
await loadJobs(payload.job_id);
} catch (error) {
setStatus("#run-status", "Error: run request failed.", "error");
setStatus("#run-status", `Error: ${formatError(error)}`, "error");
setState("#run-state", "idle");
} finally {
button.disabled = false;
Expand Down Expand Up @@ -225,7 +249,7 @@ async function importResult() {
renderImportedResult();
await loadCompare();
} catch (error) {
setStatus("#import-status", "Error: import failed.", "error");
setStatus("#import-status", `Error: ${formatError(error)}`, "error");
setState("#import-state", "idle");
} finally {
button.disabled = false;
Expand All @@ -244,7 +268,7 @@ async function loadJetsonCommand() {
setState("#jetson-state", "completed");
} catch (error) {
textarea.value = "";
setStatus("#jetson-status", "Error: command unavailable.", "error");
setStatus("#jetson-status", `Error: ${formatError(error)}`, "error");
setState("#jetson-state", "idle");
}
}
Expand Down Expand Up @@ -616,10 +640,20 @@ async function initLocalStudio() {
await loadJetsonCommand();
} catch (error) {
console.error("Local Studio initialization failed", error);
if (window.location.protocol === "file:") {
renderFileProtocolNotice();
}
renderSafeFallback();
}
}

function renderFileProtocolNotice() {
const message = "Open http://127.0.0.1:8000/studio instead of this file path to use Run, Import, Compare, and Jetson helpers.";
setStatus("#run-status", message, "error");
setStatus("#import-status", message, "error");
setStatus("#jetson-status", message, "error");
}

function renderSafeFallback() {
const requiredTargets = [
["#pipeline-flow", "Pipeline cards could not be initialized."],
Expand All @@ -637,6 +671,11 @@ function renderSafeFallback() {
});
}

function formatError(error) {
const message = error?.message || String(error || "request failed");
return message.replace(/^Error:\s*/, "");
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initLocalStudio);
} else {
Expand Down
6 changes: 3 additions & 3 deletions inferedgelab/studio/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
}
}
</style>
<link rel="stylesheet" href="/studio/static/style.css?v=5" />
<link rel="stylesheet" href="/studio/static/style.css?v=6" />
</head>
<body>
<main class="shell">
Expand Down Expand Up @@ -191,7 +191,7 @@ <h3 id="import-title">Runtime result JSON</h3>
</div>
<div class="form-stack">
<label for="import-json-path">result path</label>
<input id="import-json-path" type="text" placeholder="results/jetson/result.json" />
<input id="import-json-path" type="text" value="results/latest.json" placeholder="results/latest.json" />
<button id="import-button" type="button">Import</button>
<p id="import-status" class="status-line"></p>
</div>
Expand Down Expand Up @@ -275,6 +275,6 @@ <h2 id="future-title">Future Work</h2>
</section>
</main>

<script src="/studio/static/app.js?v=5" defer></script>
<script src="/studio/static/app.js?v=6" defer></script>
</body>
</html>
16 changes: 16 additions & 0 deletions tests/test_studio_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def test_studio_route_returns_local_studio_html():
assert 'data-critical="studio-dark"' in html
assert 'href="/studio/static/style.css?v=' in html
assert 'src="/studio/static/app.js?v=' in html
assert 'value="results/latest.json"' in html


def test_studio_static_assets_are_served():
Expand Down Expand Up @@ -66,6 +67,8 @@ def test_studio_static_assets_include_redesigned_ui_contracts():
assert style_response.status_code == 200
assert "initLocalStudio" in app_text
assert "DOMContentLoaded" in app_text
assert "Open Studio from http://127.0.0.1:8000/studio" in app_text
assert "responseErrorMessage" in app_text
assert "#0b0f14" in style_text
assert "grid-template-columns" in style_text
assert ".form-stack button" in style_text
Expand Down Expand Up @@ -174,6 +177,19 @@ def test_studio_import_api_accepts_runtime_result_json():
assert response["compare_ready"] is False


def test_studio_import_api_accepts_existing_result_path():
app = api.create_app()
route = _get_route(app, "/studio/api/import")
request = SimpleNamespace(app=app)

response = route.endpoint(request=request, payload={"path": "results/latest.json"})

assert response["status"] == "imported"
assert response["result"]["compare_key"]
assert response["result"]["backend_key"]
assert response["compare_ready"] is False


def test_studio_jetson_command_api_returns_command():
app = api.create_app()
route = _get_route(app, "/studio/api/jetson-command")
Expand Down
Loading