From 4e165c1a5c941933122df23d31e6b3c688d85229 Mon Sep 17 00:00:00 2001 From: hyeokjun32 Date: Wed, 29 Apr 2026 23:36:58 +0900 Subject: [PATCH] fix: improve local studio import feedback --- inferedgelab/studio/static/app.js | 51 +++++++++++++++++++++++---- inferedgelab/studio/static/index.html | 6 ++-- tests/test_studio_routes.py | 16 +++++++++ 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/inferedgelab/studio/static/app.js b/inferedgelab/studio/static/app.js index cdb8f06..1d6d22a 100644 --- a/inferedgelab/studio/static/app.js +++ b/inferedgelab/studio/static/app.js @@ -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: { @@ -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 { @@ -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; @@ -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; @@ -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"); } } @@ -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."], @@ -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 { diff --git a/inferedgelab/studio/static/index.html b/inferedgelab/studio/static/index.html index ea9c98f..1085d7a 100644 --- a/inferedgelab/studio/static/index.html +++ b/inferedgelab/studio/static/index.html @@ -132,7 +132,7 @@ } } - +
@@ -191,7 +191,7 @@

Runtime result JSON

- +

@@ -275,6 +275,6 @@

Future Work

- + diff --git a/tests/test_studio_routes.py b/tests/test_studio_routes.py index 691f426..f59956a 100644 --- a/tests/test_studio_routes.py +++ b/tests/test_studio_routes.py @@ -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(): @@ -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 @@ -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")