diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dcd07a3..6049a59 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,277 +1,158 @@ import { useEffect, useMemo, useState } from "react"; import { callSkill, - fileToBrowserDocument, loadAgentConfig, loadSession, - maybeAuthorize, + sampleArgs, signInUrl, } from "./a2a.js"; -const MAX_FILE_BYTES = 64 * 1024; -const MAX_FILES = 4; -const ACCEPT = ".json,.csv,.txt,application/json,text/csv,text/plain"; - -const SUCCESS_FIXTURE = [ - { vendor: "Acme Bearings", quantity: 500, unit_price: 10.75, delivery_days: 12, warranty_months: 12 }, - { vendor: "Beta Industrial", quantity: 500, unit_price: 9.1, delivery_days: 8, warranty_months: 18 }, -]; - -const EMPTY_QUOTE = { vendor: "", quantity: 1, unit_price: 0, delivery_days: 0, warranty_months: 0 }; -const DEFAULT_WEIGHTS = { price: 50, delivery: 30, warranty: 20 }; - export function App() { const [config, setConfig] = useState(null); const [session, setSession] = useState(null); - const [loadError, setLoadError] = useState(null); - const [comparisonId, setComparisonId] = useState("studio-quote-v1"); - const [quotes, setQuotes] = useState(SUCCESS_FIXTURE); - const [weights, setWeights] = useState(DEFAULT_WEIGHTS); - const [files, setFiles] = useState([]); + const [selectedSkillName, setSelectedSkillName] = useState(""); + const [argsText, setArgsText] = useState("{}"); const [result, setResult] = useState(null); - const [historyResult, setHistoryResult] = useState(null); const [error, setError] = useState(null); const [running, setRunning] = useState(false); 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)); } + return loadSession(data).catch(() => null); }) - .catch((err) => setLoadError(err.message || String(err))); + .then((sessionData) => setSession(sessionData)) + .catch((err) => setError(err.message || String(err))); }, []); - const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated); - const signInHref = useMemo(() => (config ? signInUrl(config) : null), [config]); - 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() { - if (quotes.length < 10) setQuotes((current) => [...current, { ...EMPTY_QUOTE }]); - } - - function removeQuote(index) { - setQuotes((current) => current.filter((_, i) => i !== index)); - } - - function normalizeQuotes() { - return quotes.map((quote) => ({ - vendor: String(quote.vendor || "").trim(), - quantity: Number(quote.quantity), - unit_price: Number(quote.unit_price), - delivery_days: Number(quote.delivery_days), - warranty_months: Number(quote.warranty_months), - })); - } - - async function runCompare(event) { + async function runSkill(event) { event.preventDefault(); - if (!config) return; - if (needsSignIn) return maybeAuthorize(config); + if (!config || !selectedSkill) return; setRunning(true); setError(null); setResult(null); try { - let data; - if (files.length) { - const docs = await Promise.all(files.map(fileToBrowserDocument)); - data = await callSkill(config, "compare_quotes_from_browser_upload", { - comparison_id: comparisonId, - documents: docs, - weights: coerceWeights(weights), - }); - } else { - data = await callSkill(config, "compare_quotes", { - comparison_id: comparisonId, - quotes: normalizeQuotes(), - weights: coerceWeights(weights), - }); - } - if (!data.ok) setError(data.code || "Quote comparison failed validation."); + const args = JSON.parse(argsText || "{}"); + const data = await callSkill(config, selectedSkill.name, args); setResult(data); } catch (err) { - if (err.status === 401) return maybeAuthorize(config); setError(err.message || String(err)); } finally { setRunning(false); } } - async function reloadComparison() { - if (!config) return; - if (needsSignIn) return maybeAuthorize(config); - setRunning(true); - setError(null); - try { - const data = await callSkill(config, "get_comparison", { comparison_id: comparisonId }); - if (!data.ok) setError(data.code || "Comparison not found."); - setHistoryResult(data); - if (data.ok) setResult(data); - } catch (err) { - if (err.status === 401) return maybeAuthorize(config); - setError(err.message || String(err)); - } finally { - setRunning(false); - } + if (!config) { + return ( +
+

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

+
+ ); } - function useFixture(kind) { - setFiles([]); - setComparisonId(kind === "failure" ? "studio-quote-invalid" : "studio-quote-v1"); - setQuotes(kind === "failure" ? [ - { vendor: "Only Vendor", quantity: 1, unit_price: 10, delivery_days: 5, warranty_months: 6 }, - ] : SUCCESS_FIXTURE); - setWeights(DEFAULT_WEIGHTS); - setResult(null); - setHistoryResult(null); - setError(null); - } - - function onFilesSelected(event) { - const picked = Array.from(event.target.files || []); - const tooLarge = picked.find((file) => file.size > MAX_FILE_BYTES); - if (picked.length > MAX_FILES) return setError(`Upload at most ${MAX_FILES} files.`); - if (tooLarge) return setError(`${tooLarge.name} exceeds 64 KiB.`); - setFiles(picked); - setError(null); - } - - if (loadError) return

{loadError}

; - if (!config) return

Loading QuoteJudge...

; - return (
-
- -
-
-

Weighted vendor quote decisions

-

Pick the best quote, save the evidence, reopen it later.

-

- Upload or paste supplier quotes, tune price/delivery/warranty weights, and persist a per-user production receipt without sending secrets to the browser. -

-
- - - -
-
-
- Current recommendation - {result?.recommendation?.vendor || "Run a comparison"} -

{result?.recommendation?.rationale || "Beta wins the bundled fixture because it is cheaper, faster, and has a longer warranty."}

-
-
-
+ -
- -
-

Durable reload

-

Saved comparison

- -
{JSON.stringify(historyResult || { message: "Run compare, then reopen the same ID in a later invocation." }, null, 2)}
+
+
+
+

Skill runner

+

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

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