From 527af0ec80143217342d9510189fe17378b74ff0 Mon Sep 17 00:00:00 2001 From: a2a-cloud Date: Sat, 18 Jul 2026 03:22:33 +0000 Subject: [PATCH] a2a-source-edit: write frontend/src/App.jsx --- frontend/src/App.jsx | 340 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 frontend/src/App.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..5c31913 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,340 @@ +import { useEffect, useMemo, useState } from "react"; +import { callSkill, fileToBrowserDocument, loadAgentConfig, loadSession, signInUrl } from "./a2a.js"; + +const DEFAULT_QUOTES = [ + { 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 DEFAULT_WEIGHTS = { price: 50, delivery: 30, warranty: 20 }; + +function blankQuote() { + return { vendor: "", quantity: 1, unit_price: 0, delivery_days: 0, warranty_months: 0 }; +} + +function normalizeQuote(quote) { + return { + 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), + }; +} + +function parsePastedQuotes(text) { + const trimmed = text.trim(); + if (!trimmed) return []; + try { + const parsed = JSON.parse(trimmed); + const rows = Array.isArray(parsed) ? parsed : parsed.quotes; + if (Array.isArray(rows)) return rows.map(normalizeQuote); + } catch { + // Fall through to CSV-ish parsing. + } + const lines = trimmed.split(/\r?\n/).filter(Boolean); + const hasHeader = /vendor|supplier/i.test(lines[0] || ""); + const rows = hasHeader ? lines.slice(1) : lines; + return rows.map((line) => { + const parts = line.split(/[,;|\t]/).map((part) => part.trim()); + return normalizeQuote({ + vendor: parts[0], + quantity: parts[1], + unit_price: parts[2], + delivery_days: parts[3], + warranty_months: parts[4], + }); + }); +} + +function validateQuotes(quotes, weights) { + const complete = quotes.map(normalizeQuote).filter((quote) => quote.vendor); + if (complete.length < 2) return "Add at least two vendor quotes before comparing."; + for (const quote of complete) { + if (!Number.isFinite(quote.quantity) || quote.quantity <= 0) return `${quote.vendor}: quantity must be greater than 0.`; + if (!Number.isFinite(quote.unit_price) || quote.unit_price < 0) return `${quote.vendor}: unit price must be 0 or greater.`; + if (!Number.isFinite(quote.delivery_days) || quote.delivery_days < 0) return `${quote.vendor}: delivery days must be 0 or greater.`; + if (!Number.isFinite(quote.warranty_months) || quote.warranty_months < 0) return `${quote.vendor}: warranty must be 0 or greater.`; + } + const totalWeight = Number(weights.price) + Number(weights.delivery) + Number(weights.warranty); + if (!Number.isFinite(totalWeight) || totalWeight <= 0) return "Set at least one positive weight."; + return null; +} + +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(DEFAULT_QUOTES); + const [weights, setWeights] = useState(DEFAULT_WEIGHTS); + const [pasteText, setPasteText] = useState(""); + const [result, setResult] = useState(null); + const [history, setHistory] = useState([]); + const [status, setStatus] = useState(""); + const [error, setError] = useState(""); + const [running, setRunning] = useState(false); + + useEffect(() => { + loadAgentConfig() + .then(async (data) => { + setConfig(data); + try { + setSession(await loadSession(data)); + } catch { + setSession({ authenticated: false }); + } + }) + .catch((err) => setLoadError(err.message || String(err))); + }, []); + + useEffect(() => { + try { + const saved = JSON.parse(window.localStorage.getItem("quotejudge.history") || "[]"); + if (Array.isArray(saved)) setHistory(saved.slice(0, 8)); + } catch { + setHistory([]); + } + }, []); + + const needsSignIn = Boolean(config?.auth?.invokeRequiresSession && session && !session.authenticated); + const authHref = useMemo(() => (config ? signInUrl(config) : null), [config]); + const validation = validateQuotes(quotes, weights); + const totalWeight = Number(weights.price) + Number(weights.delivery) + Number(weights.warranty); + + function updateQuote(index, field, value) { + setQuotes((current) => current.map((quote, i) => (i === index ? { ...quote, [field]: value } : quote))); + } + + function remember(id) { + const next = [id, ...history.filter((item) => item !== id)].slice(0, 8); + setHistory(next); + window.localStorage.setItem("quotejudge.history", JSON.stringify(next)); + } + + function applyPastedQuotes() { + setError(""); + try { + const parsed = parsePastedQuotes(pasteText); + if (parsed.length < 2) { + setError("Paste JSON, CSV, or pipe-separated text with at least two quotes."); + return; + } + setQuotes(parsed); + setStatus(`Loaded ${parsed.length} pasted quotes.`); + } catch (err) { + setError(err.message || "Could not parse pasted quotes."); + } + } + + async function compare(event) { + event.preventDefault(); + if (!config) return; + setError(""); + setStatus(""); + const message = validateQuotes(quotes, weights); + if (message) { + setError(message); + return; + } + setRunning(true); + try { + const data = await callSkill(config, "compare_quotes", { + comparison_id: comparisonId, + quotes: quotes.map(normalizeQuote).filter((quote) => quote.vendor), + weights: { + price: Number(weights.price), + delivery: Number(weights.delivery), + warranty: Number(weights.warranty), + }, + }); + setResult(data); + if (data.ok) { + remember(data.comparison_id); + setStatus("Comparison saved. You can refresh and reopen it by ID."); + } else { + setError(data.message || data.code || "Comparison failed."); + } + } catch (err) { + if (err.status === 401 && authHref) setError("Session expired. Sign in again to run QuoteJudge."); + else setError(err.message || String(err)); + } finally { + setRunning(false); + } + } + + async function reopen(id = comparisonId) { + if (!config || !id) return; + setRunning(true); + setError(""); + setStatus(""); + try { + const data = await callSkill(config, "get_comparison", { comparison_id: id }); + setResult(data); + setComparisonId(id); + if (data.ok) { + remember(id); + setStatus("Saved comparison reopened from managed Postgres."); + } else { + setError(data.message || "No saved comparison found for this ID."); + } + } catch (err) { + if (err.status === 401 && authHref) setError("Session expired. Sign in again to reopen comparisons."); + else setError(err.message || String(err)); + } finally { + setRunning(false); + } + } + + async function handleFiles(event) { + if (!config) return; + const files = Array.from(event.target.files || []); + if (!files.length) return; + setRunning(true); + setError(""); + setStatus(""); + try { + const documents = await Promise.all(files.map((file) => fileToBrowserDocument(file))); + const data = await callSkill(config, "compare_uploaded_quotes_browser", { + comparison_id: comparisonId, + documents, + weights: { + price: Number(weights.price), + delivery: Number(weights.delivery), + warranty: Number(weights.warranty), + }, + }); + setResult(data); + if (data.ok) { + setQuotes(data.quotes.map(({ vendor, quantity, unit_price, delivery_days, warranty_months }) => ({ vendor, quantity, unit_price, delivery_days, warranty_months }))); + remember(data.comparison_id); + setStatus("Uploaded quotes parsed, compared, and saved."); + } else { + setError(data.message || data.code || "Upload comparison failed."); + } + } catch (err) { + setError(err.message || String(err)); + } finally { + event.target.value = ""; + setRunning(false); + } + } + + if (!config) { + return

{loadError || "Loading QuoteJudge…"}

; + } + + return ( +
+
+
+

QuoteJudge Studio

+

Pick the best vendor quote with weighted, auditable scoring.

+

Upload or paste quotes, tune price/delivery/warranty importance, save the comparison, and reopen it after refresh. Results are calculated by typed A2A backend tools and stored per signed-in user.

+
+
+ +
+ {session?.authenticated ? "Signed in" : "Sign-in required to run"} +

{session?.user?.email || session?.org?.slug || "Platform auth protects saved comparisons."}

+
+ {needsSignIn && authHref ? Sign in : null} +
+
+ +
+
+
+
+

Inputs

+

Vendor quotes

+
+ +
+ + + +
+ {quotes.map((quote, index) => ( +
+ + + + + + +
+ ))} +
+ +
+