Files
agents-marketplace/buyer/src/ui/components/JobEstimate.tsx
FilipBl4gojevic 73700f983a Brand vocabulary scrub: settlement framing, UI strings, retire M1-era design docs
- docs/ARCHITECTURE.md: settlement-asset framing (AP3X bonded in escrow,
  settles on accepted work); remove references to external network
  adapters that are not part of the project
- docs/buyer + docs/supplier READMEs: commission/bond/settle vocabulary
- Buyer web UI: "Accept & Settle", escrow-framed labels, gateway-managed
  wallet wording; gateway landing title punctuation
- Delete EXECUTIVE_SUMMARY and local-llm-marketplace docs + exports:
  M1-era internal design snapshots superseded by ARCHITECTURE.md and the
  public docs site

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:23:35 +02:00

134 lines
4.6 KiB
TypeScript

/**
* buyer/src/ui/components/JobEstimate.tsx — step 2: show the pre-spend cost
* estimate and require an explicit "Proceed" before any escrow is posted.
*
* GET /v1/pdf-jobs/:id/estimate returns the map+reduce call count, an
* upper-bound AP3X cost, the wallet balance, and whether the spend would drop
* the wallet below its floor (in which case Proceed is disabled).
*/
import { useEffect, useState } from "react";
import type { UploadInfo } from "./PdfUploadForm.js";
interface EstimateData {
mapCalls: number;
reduceCalls: number;
totalCalls: number;
perCallMaxLovelace: string;
totalLovelace: string;
totalAp3x: string;
suppliers: { model: string; price_lovelace: string }[];
wallet_balance_lovelace: string;
wallet_floor_lovelace: string;
would_drop_below_floor: boolean;
no_capable_suppliers: boolean;
}
function ap3x(lovelace: string): string {
return (Number(lovelace) / 1e6).toFixed(2);
}
export default function JobEstimate({
info,
onStarted,
}: {
info: UploadInfo;
onStarted: () => void;
}) {
const [est, setEst] = useState<EstimateData | null>(null);
const [error, setError] = useState<string | null>(null);
const [starting, setStarting] = useState(false);
useEffect(() => {
let cancelled = false;
fetch(`/v1/pdf-jobs/${info.job_id}/estimate`, { credentials: "same-origin" })
.then(async (r) => {
const body = await r.json();
if (!r.ok) throw new Error(`${body.error}: ${body.message ?? ""}`);
if (!cancelled) setEst(body as EstimateData);
})
.catch((e: Error) => !cancelled && setError(e.message));
return () => {
cancelled = true;
};
}, [info.job_id]);
const proceed = async (): Promise<void> => {
setStarting(true);
setError(null);
try {
const r = await fetch(`/v1/pdf-jobs/${info.job_id}/start`, {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: true }),
});
const body = await r.json();
if (!r.ok) throw new Error(`${body.error}: ${body.message ?? ""}`);
onStarted();
} catch (e) {
setError((e as Error).message);
setStarting(false);
}
};
if (error) {
return (
<div role="alert" className="rounded border border-red-300 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
);
}
if (!est) return <div className="text-sm text-gray-500">Estimating cost…</div>;
const blocked = est.would_drop_below_floor || est.no_capable_suppliers || est.totalCalls === 0;
return (
<div className="space-y-4">
<div className="rounded border border-gray-200 bg-white p-4 text-sm">
<h3 className="mb-2 font-medium">{info.filename}</h3>
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-gray-700">
<dt>Pages</dt>
<dd className="text-right">{info.page_count}</dd>
<dt>Chunks (map calls)</dt>
<dd className="text-right">{est.mapCalls}</dd>
<dt>Reduce calls</dt>
<dd className="text-right">{est.reduceCalls}</dd>
<dt className="font-medium">Total escrowed calls</dt>
<dd className="text-right font-medium">{est.totalCalls}</dd>
<dt className="font-medium">Est. max cost</dt>
<dd className="text-right font-medium">{est.totalAp3x} AP3X</dd>
<dt>Wallet balance</dt>
<dd className="text-right">{ap3x(est.wallet_balance_lovelace)} AP3X</dd>
<dt>Wallet floor</dt>
<dd className="text-right">{ap3x(est.wallet_floor_lovelace)} AP3X</dd>
</dl>
<p className="mt-3 text-xs text-gray-500">
Suppliers: {est.suppliers.map((s) => `${s.model} (${ap3x(s.price_lovelace)})`).join(", ") || "none"}
</p>
</div>
{est.no_capable_suppliers && (
<div className="rounded border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-900">
No capable suppliers are available right now — try again shortly.
</div>
)}
{est.would_drop_below_floor && (
<div className="rounded border border-red-300 bg-red-50 p-3 text-sm text-red-700">
This job would drop the wallet below its floor. Fund the wallet or
reduce the book size before proceeding.
</div>
)}
<button
type="button"
onClick={() => void proceed()}
disabled={blocked || starting}
className="rounded bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:bg-gray-400"
>
{starting ? "Starting…" : `Proceed (locks ~${est.totalAp3x} AP3X in escrow)`}
</button>
</div>
);
}