diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index 5c31913..0000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -1,340 +0,0 @@ -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) => ( -
- - - - - - -
- ))} -
- -
-