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) => (
))}