diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7b4deb4..6049a59 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,253 +1,158 @@ import { useEffect, useMemo, useState } from "react"; import { - callSkillWithSession, - fileToBrowserDocument, + callSkill, loadAgentConfig, loadSession, + sampleArgs, signInUrl, } from "./a2a.js"; -const MAX_UPLOAD_BYTES = 256000; -const STARTER_QUOTES = [ - { vendor: "Acme Bearings", unit_price: 10.75, quantity: 500, delivery_days: 12, warranty_months: 12, currency: "BRL" }, - { vendor: "Beta Industrial", unit_price: 9.1, quantity: 500, delivery_days: 8, warranty_months: 18, currency: "BRL" }, -]; - export function App() { const [config, setConfig] = useState(null); const [session, setSession] = useState(null); - const [comparisonId, setComparisonId] = useState("studio-quote-v1"); - const [quotes, setQuotes] = useState(STARTER_QUOTES); - const [weights, setWeights] = useState({ price: 50, delivery: 30, warranty: 20 }); + const [selectedSkillName, setSelectedSkillName] = useState(""); + const [argsText, setArgsText] = useState("{}"); const [result, setResult] = useState(null); - const [status, setStatus] = useState("Loading QuoteJudge Studio…"); + const [error, setError] = useState(null); const [running, setRunning] = useState(false); - const [files, setFiles] = useState([]); useEffect(() => { loadAgentConfig() - .then(async (data) => { + .then((data) => { setConfig(data); - try { - setSession(await loadSession(data)); - } catch { - setSession({ authenticated: false }); + const firstSkill = data.skills?.[0]; + if (firstSkill) { + setSelectedSkillName(firstSkill.name); + setArgsText(JSON.stringify(sampleArgs(firstSkill), null, 2)); } - setStatus("Ready to compare vendor quotes."); + return loadSession(data).catch(() => null); }) - .catch((error) => setStatus(error.message || String(error))); + .then((sessionData) => setSession(sessionData)) + .catch((err) => setError(err.message || String(err))); }, []); - const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated); - const signInHref = useMemo(() => (config && needsSignIn ? signInUrl(config) : null), [config, needsSignIn]); - const totalWeight = Number(weights.price || 0) + Number(weights.delivery || 0) + Number(weights.warranty || 0); + const selectedSkill = useMemo( + () => config?.skills?.find((skill) => skill.name === selectedSkillName), + [config, selectedSkillName], + ); - function updateQuote(index, field, value) { - setQuotes((current) => current.map((quote, i) => (i === index ? { ...quote, [field]: value } : quote))); + 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); } - function addQuote() { - setQuotes((current) => [...current, { vendor: "", unit_price: 0, quantity: 1, delivery_days: 0, warranty_months: 0, currency: "BRL" }]); - } - - function removeQuote(index) { - setQuotes((current) => current.filter((_, i) => i !== index)); - } - - function normalizeQuotes() { - return quotes.map((quote) => ({ - vendor: String(quote.vendor || "").trim(), - unit_price: Number(quote.unit_price), - quantity: Number(quote.quantity), - delivery_days: Number(quote.delivery_days), - warranty_months: Number(quote.warranty_months), - currency: quote.currency || "BRL", - })); - } - - async function runCompare(event) { + async function runSkill(event) { event.preventDefault(); - if (!config) return; + if (!config || !selectedSkill) return; setRunning(true); + setError(null); setResult(null); - setStatus("Scoring quotes and saving result…"); try { - const data = await callSkillWithSession(config, "compare_quotes", { - comparison_id: comparisonId, - quotes: normalizeQuotes(), - weights: { - price: Number(weights.price), - delivery: Number(weights.delivery), - warranty: Number(weights.warranty), - }, - }); + const args = JSON.parse(argsText || "{}"); + const data = await callSkill(config, selectedSkill.name, args); setResult(data); - setStatus(data.ok ? `Saved ${data.comparison_id}. Recommendation: ${data.recommendation.vendor}.` : data.message); - } catch (error) { - setStatus(error.message || String(error)); - } finally { - setRunning(false); - } - } - - async function reopen() { - if (!config) return; - setRunning(true); - setResult(null); - setStatus("Reopening saved comparison…"); - try { - const data = await callSkillWithSession(config, "get_comparison", { comparison_id: comparisonId }); - setResult(data); - setStatus(data.ok ? `Reopened ${data.comparison_id}.` : data.message); - } catch (error) { - setStatus(error.message || String(error)); - } finally { - setRunning(false); - } - } - - async function uploadAndCompare() { - if (!config || !files.length) return; - setRunning(true); - setResult(null); - setStatus("Encoding browser upload and comparing…"); - try { - const tooLarge = files.find((file) => file.size > MAX_UPLOAD_BYTES); - if (tooLarge) throw new Error(`${tooLarge.name} exceeds ${MAX_UPLOAD_BYTES} bytes.`); - const documents = await Promise.all(files.map(fileToBrowserDocument)); - const data = await callSkillWithSession(config, "compare_uploaded_quotes_browser", { - comparison_id: comparisonId, - documents, - weights: { - price: Number(weights.price), - delivery: Number(weights.delivery), - warranty: Number(weights.warranty), - }, - }); - setResult(data); - setStatus(data.ok ? `Uploaded and saved ${data.comparison_id}.` : data.message); - } catch (error) { - setStatus(error.message || String(error)); + } catch (err) { + setError(err.message || String(err)); } finally { setRunning(false); } } if (!config) { - return

{status}

; + return ( +
+

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

+
+ ); } return ( -
-
+
+
-
-
- - - - -
-

0 ? "hint" : "hint warning"}>Total weight: {totalWeight}. Values are normalized by the backend.

- -
-
- VendorUnit priceQtyDelivery daysWarranty months -
- {quotes.map((quote, index) => ( -
- updateQuote(index, "vendor", event.target.value)} /> - updateQuote(index, "unit_price", event.target.value)} /> - updateQuote(index, "quantity", event.target.value)} /> - updateQuote(index, "delivery_days", event.target.value)} /> - updateQuote(index, "warranty_months", event.target.value)} /> - -
+
+ -
- - - +
+ {session?.authenticated ? "Signed in" : needsSignIn ? "Signed out" : "Local session"} + {session?.user?.email || session?.org?.slug || "dev@example.local"} + {/* Skills that spend the caller's LLM budget need a signed-in + visitor even when the page itself is public, so offer the way in + rather than failing at the first click. */} + {needsSignIn && signInHref ? ( + Sign in to run + ) : null}
- + -
-
-

Browser upload bridge

-

Upload bounded JSON, CSV, or text files. The browser sends base64 JSON to compare_uploaded_quotes_browser; external clients can still use compare_uploaded_quote_file.

-
- setFiles(Array.from(event.target.files || []))} /> - +
+
+
+

Skill runner

+

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

+
+ + Docs + +
+ + {selectedSkill ? ( +
+