From 73f84500d35453c6d79ec589952523746df45e86 Mon Sep 17 00:00:00 2001 From: A2A Cloud Operator Date: Sun, 19 Jul 2026 12:41:49 -0300 Subject: [PATCH] fix frontend SVG results and downloads --- frontend/package.json | 3 ++- frontend/src/App.jsx | 17 ++++++++++++++++- frontend/src/a2a.js | 12 ++++++++++-- frontend/src/style.css | 4 +++- frontend/tests/a2a.test.mjs | 21 +++++++++++++++++++++ tests/test_full_stack_contract.py | 3 +++ 6 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 frontend/tests/a2a.test.mjs diff --git a/frontend/package.json b/frontend/package.json index 8941df0..5f7415f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite --host 0.0.0.0 --port 5173", "build": "vite build", - "preview": "vite preview --host 0.0.0.0" + "preview": "vite preview --host 0.0.0.0", + "test": "node --test" }, "dependencies": { "react": "^18.3.1", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e4c27b0..750b502 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,18 @@ const DEFAULT_FORM = { variant_count: 2, }; +function downloadSvgAsset(asset) { + const blob = new Blob([asset.svg], { type: asset.media_type || "image/svg+xml;charset=utf-8" }); + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = objectUrl; + link.download = asset.name || "game-asset.svg"; + document.body.appendChild(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0); +} + export function App() { const [config, setConfig] = useState(null); const [session, setSession] = useState(null); @@ -201,7 +213,10 @@ export function App() {
{asset.name} - +
+ + +
))} diff --git a/frontend/src/a2a.js b/frontend/src/a2a.js index 0a8d87e..9aed978 100644 --- a/frontend/src/a2a.js +++ b/frontend/src/a2a.js @@ -1,4 +1,11 @@ -export const CONFIG_URL = import.meta.env.DEV ? "/config.json" : "./config.json"; +export const CONFIG_URL = import.meta.env?.DEV ? "/config.json" : "./config.json"; + +export function unwrapSkillResponse(payload) { + if (payload && typeof payload === "object" && "result" in payload) { + return payload.result; + } + return payload; +} export async function requestJson(url, init = {}) { const response = await fetch(url, { credentials: "same-origin", ...init }); @@ -43,11 +50,12 @@ export async function callSkill(config, skillName, args) { if (config.auth?.invokeRequiresSession) { await requireSession(config); } - const data = await requestJson(`${config.endpoints.invoke}/${encodeURIComponent(skillName)}`, { + const payload = await requestJson(`${config.endpoints.invoke}/${encodeURIComponent(skillName)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ arguments: args }), }); + const data = unwrapSkillResponse(payload); if (data?.status === "setup_required") { throw new Error(data.warning || "Backend setup is required before this action can run."); } diff --git a/frontend/src/style.css b/frontend/src/style.css index 993f26f..9ff59b5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -46,5 +46,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monos .svg-preview svg { max-width: 100%; height: auto; image-rendering: pixelated; } .asset-meta { padding: 14px; align-items: start; } .asset-meta strong { overflow-wrap: anywhere; } +.asset-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; } +.asset-meta button.secondary { border: 1px solid rgba(148, 163, 184, .35); background: rgba(15, 23, 42, .88); color: #dcecff; } @media (max-width: 900px) { .hero, .workspace { grid-template-columns: 1fr; } h1 { font-size: clamp(36px, 13vw, 64px); } } -@media (max-width: 560px) { .form-grid { grid-template-columns: 1fr; } .app-shell { width: min(100% - 20px, 1180px); padding-top: 16px; } .receipt-header, .result-header, .asset-meta { align-items: flex-start; flex-direction: column; } } +@media (max-width: 560px) { .form-grid { grid-template-columns: 1fr; } .app-shell { width: min(100% - 20px, 1180px); padding-top: 16px; } .receipt-header, .result-header, .asset-meta { align-items: flex-start; flex-direction: column; } .asset-actions { justify-content: flex-start; } } diff --git a/frontend/tests/a2a.test.mjs b/frontend/tests/a2a.test.mjs new file mode 100644 index 0000000..91bc293 --- /dev/null +++ b/frontend/tests/a2a.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { unwrapSkillResponse } from "../src/a2a.js"; + +test("unwraps the production invoke response envelope", () => { + const result = { + status: "ok", + svg_assets: [{ name: "sprite.svg", svg: "" }], + }; + + assert.deepEqual( + unwrapSkillResponse({ result, events: [], artifacts: [], grant_id: null }), + result, + ); +}); + +test("keeps direct skill responses compatible", () => { + const result = { status: "ok", receipts: [] }; + assert.equal(unwrapSkillResponse(result), result); +}); diff --git a/tests/test_full_stack_contract.py b/tests/test_full_stack_contract.py index 27eb4a0..ef12f4e 100644 --- a/tests/test_full_stack_contract.py +++ b/tests/test_full_stack_contract.py @@ -17,6 +17,7 @@ def test_full_stack_product_contract(): manifest = (ROOT / "a2a.yaml").read_text(encoding="utf-8") source = (ROOT / "agent.py").read_text(encoding="utf-8") frontend = (ROOT / "frontend" / "src" / "App.jsx").read_text(encoding="utf-8") + frontend_client = (ROOT / "frontend" / "src" / "a2a.js").read_text(encoding="utf-8") assert "frontend:" in manifest and "mount: /app" in manifest assert "public: false" in manifest @@ -26,6 +27,8 @@ def test_full_stack_product_contract(): assert "AgentPlatformResources" in source and "AgentDatabase(" in source assert "Skill runner" not in frontend assert "generate_svg_asset" in frontend + assert "Download SVG" in frontend + assert "unwrapSkillResponse(payload)" in frontend_client assert "DATABASE_URL" not in frontend migrations = list((ROOT / "db" / "migrations").glob("*.sql"))