diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9e9b624..200071e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,158 +1,254 @@ import { useEffect, useMemo, useState } from "react"; -import { - callSkill, - loadAgentConfig, - loadSession, - sampleArgs, - signInUrl, -} from "./a2a.js"; +import { callSkill, loadAgentConfig, loadSession, signInUrl } from "./a2a.js"; + +const DEFAULT_SCHEMA = JSON.stringify( + { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string", format: "email" }, + age: { type: "integer" }, + }, + required: ["name", "email"], + additionalProperties: false, + }, + null, + 2, +); + +const MAX_UPLOAD_BYTES = 2 * 1024 * 1024; export function App() { const [config, setConfig] = useState(null); const [session, setSession] = useState(null); - const [selectedSkillName, setSelectedSkillName] = useState(""); - const [argsText, setArgsText] = useState("{}"); + const [loadError, setLoadError] = useState(null); + const [file, setFile] = useState(null); + const [schemaText, setSchemaText] = useState(DEFAULT_SCHEMA); + const [workflowName, setWorkflowName] = useState("customer-normalizer"); const [result, setResult] = useState(null); + const [history, setHistory] = useState([]); const [error, setError] = useState(null); const [running, setRunning] = useState(false); + const [loadingHistory, setLoadingHistory] = useState(false); useEffect(() => { loadAgentConfig() - .then((data) => { + .then(async (data) => { setConfig(data); - const firstSkill = data.skills?.[0]; - if (firstSkill) { - setSelectedSkillName(firstSkill.name); - setArgsText(JSON.stringify(sampleArgs(firstSkill), null, 2)); + try { + const sessionData = await loadSession(data); + setSession(sessionData); + } catch { + setSession({ authenticated: false }); } - return loadSession(data).catch(() => null); }) - .then((sessionData) => setSession(sessionData)) - .catch((err) => setError(err.message || String(err))); + .catch((err) => setLoadError(err.message || String(err))); }, []); - const selectedSkill = useMemo( - () => config?.skills?.find((skill) => skill.name === selectedSkillName), - [config, selectedSkillName], - ); + const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated); + const signin = useMemo(() => (config ? signInUrl(config) : null), [config]); - const needsSignIn = Boolean( - config?.auth?.invokeRequiresSession && session && !session.authenticated, - ); - const signInHref = useMemo( - () => (config && needsSignIn ? signInUrl(config) : null), - [config, needsSignIn], - ); - - function selectSkill(skill) { - setSelectedSkillName(skill.name); - setArgsText(JSON.stringify(sampleArgs(skill), null, 2)); - setResult(null); - setError(null); + async function refreshHistory() { + if (!config || needsSignIn) return; + setLoadingHistory(true); + try { + const data = await callSkill(config, "list_workflow_receipts", { limit: 10 }); + setHistory(data.receipts || []); + } catch (err) { + if ((err.message || "").includes("401")) setSession({ authenticated: false }); + } finally { + setLoadingHistory(false); + } } - async function runSkill(event) { + useEffect(() => { + if (config && session?.authenticated) refreshHistory(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config, session?.authenticated]); + + async function submitWorkflow(event) { event.preventDefault(); - if (!config || !selectedSkill) return; - setRunning(true); setError(null); setResult(null); + + if (needsSignIn) { + setError("Sign in to run this workflow builder."); + return; + } + if (!file) { + setError("Choose a CSV, Excel, JSON, JSON-LD, or text file first."); + return; + } + if (file.size > MAX_UPLOAD_BYTES) { + setError(`File is too large. The browser bridge limit is ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB.`); + return; + } + + let outputSchema; try { - const args = JSON.parse(argsText || "{}"); - const data = await callSkill(config, selectedSkill.name, args); - setResult(data); + outputSchema = JSON.parse(schemaText); } catch (err) { - setError(err.message || String(err)); + setError(`Output schema is not valid JSON: ${err.message}`); + return; + } + + setRunning(true); + try { + const dataBase64 = await fileToBase64(file); + const data = await callSkill(config, "create_workflow_from_browser_upload", { + source_file: { + filename: file.name, + media_type: file.type || "application/octet-stream", + data_base64: dataBase64, + }, + output_schema: outputSchema, + workflow_name: workflowName || "data-transform-workflow", + }); + setResult(data); + await refreshHistory(); + } catch (err) { + const message = err.message || String(err); + if (message.includes("401")) setSession({ authenticated: false }); + setError(message); } finally { setRunning(false); } } + if (loadError) { + return

{loadError}

; + } if (!config) { - return ( -
-

{error || "Loading A2A agent contract..."}

-
- ); + return

Loading workflow builder…

; } return ( -
- + -
-
-
-

Skill runner

-

{selectedSkill?.name || "No skills found"}

+
+
+
+ + setWorkflowName(event.target.value)} + placeholder="customer-normalizer" + />
- - Docs - -
- {selectedSkill ? ( - -