From e62c9af42340dcb381f577f64c0f951f1d50365a Mon Sep 17 00:00:00 2001 From: leora Date: Thu, 8 Oct 2026 19:46:14 +0800 Subject: [PATCH] React + Vite frontend (WIP): component registry, theme engine port, server serves static/dist, deploy builds --- .gitignore | 1 + deploy.sh | 5 + server.py | 11 +- static/app.js | 238 ---- static/index.html | 51 - static/theme.js | 174 --- tests/e2e_http.py | 7 +- web/index.html | 16 + web/package-lock.json | 1722 ++++++++++++++++++++++++++++ web/package.json | 22 + web/src/App.tsx | 178 +++ web/src/api.ts | 13 + web/src/components/Chat.tsx | 31 + web/src/components/ListingForm.tsx | 37 + web/src/components/Listings.tsx | 87 ++ web/src/components/Payments.tsx | 93 ++ web/src/components/registry.tsx | 33 + web/src/context.ts | 16 + web/src/main.tsx | 6 + {static => web/src}/styles.css | 0 web/src/theme/engine.ts | 170 +++ web/src/types.ts | 26 + web/tsconfig.json | 17 + web/vite.config.ts | 10 + 24 files changed, 2497 insertions(+), 467 deletions(-) delete mode 100644 static/app.js delete mode 100644 static/index.html delete mode 100644 static/theme.js create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/src/App.tsx create mode 100644 web/src/api.ts create mode 100644 web/src/components/Chat.tsx create mode 100644 web/src/components/ListingForm.tsx create mode 100644 web/src/components/Listings.tsx create mode 100644 web/src/components/Payments.tsx create mode 100644 web/src/components/registry.tsx create mode 100644 web/src/context.ts create mode 100644 web/src/main.tsx rename {static => web/src}/styles.css (100%) create mode 100644 web/src/theme/engine.ts create mode 100644 web/src/types.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.gitignore b/.gitignore index c96d75d..87fa21b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ node_modules/ screenshots/ .env +static/dist/ diff --git a/deploy.sh b/deploy.sh index 93b5b09..b56ade1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -22,6 +22,11 @@ set -a [ -f "$CONF/llm.env" ] && . "$CONF/llm.env" [ -f "$CONF/$ENVN.env" ] && . "$CONF/$ENVN.env" set +a +# Build the React frontend (web/ -> static/dist) BEFORE stopping the running server, so a failed build +# leaves the current version up. Reproducible: exact versions from web/package-lock.json via `npm ci`. +command -v npm >/dev/null || { echo "npm not found: Node 20+ is needed to build the frontend"; exit 1; } +( cd web && npm ci --no-audit --no-fund --loglevel=error && npm run build --silent ) || { echo "frontend build failed; still running the previous version"; exit 1; } +[ -f static/dist/index.html ] || { echo "build produced no static/dist/index.html"; exit 1; } PID=$(ss -ltnp 2>/dev/null | grep ":$PORT " | grep -o 'pid=[0-9]*' | cut -d= -f2 || true) [ -n "$PID" ] && kill "$PID" && sleep 0.5 AGENT_MARKET_PORT=$PORT AGENT_MARKET_ENV=$ENVN AGENT_MARKET_DATA=$ROOT/data \ diff --git a/server.py b/server.py index b6510e1..00930c4 100755 --- a/server.py +++ b/server.py @@ -14,7 +14,9 @@ HOST = os.environ.get("AGENT_MARKET_HOST", "127.0.0.1") PORT = int(os.environ.get("AGENT_MARKET_PORT", "8796")) DATA_DIR = os.environ.get("AGENT_MARKET_DATA", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")) ENV_LABEL = os.environ.get("AGENT_MARKET_ENV", "demo") +# Frontend: the React/Vite build in static/dist (built from web/ by deploy.sh; see README "Frontend"). STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") +INDEX = os.path.join("dist", "index.html") WEBHOOK_SECRET = os.environ.get("AGENT_MARKET_WEBHOOK_SECRET") or secrets.token_hex(16) # per-process if unset MAX_BODY = 64 * 1024 CHAT_RPM = int(os.environ.get("AGENT_MARKET_CHAT_RPM", "12")) # per-IP chat requests per minute @@ -264,10 +266,13 @@ class H(BaseHTTPRequestHandler): def _static(self, rel): path = os.path.normpath(os.path.join(STATIC, rel)) - if not path.startswith(STATIC) or not os.path.isfile(path): + if not path.startswith(STATIC + os.sep) or not os.path.isfile(path): + if rel == INDEX: + return self._json(503, {"error": "frontend_not_built", "hint": "cd web && npm ci && npm run build"}) return self._json(404, {"error": "not_found"}) ctype = {".html": "text/html; charset=utf-8", ".js": "application/javascript", ".css": "text/css", - ".svg": "image/svg+xml", ".png": "image/png"}.get(os.path.splitext(path)[1], "application/octet-stream") + ".svg": "image/svg+xml", ".png": "image/png", ".json": "application/json", + ".ico": "image/x-icon", ".woff2": "font/woff2"}.get(os.path.splitext(path)[1], "application/octet-stream") with open(path, "rb") as f: b = f.read() self.send_response(200) @@ -281,7 +286,7 @@ class H(BaseHTTPRequestHandler): u = urllib.parse.urlparse(self.path) path, qs = u.path, dict(urllib.parse.parse_qsl(u.query)) if path in ("/", "/index.html"): - return self._static("index.html") + return self._static(INDEX) if path.startswith("/static/"): return self._static(path[len("/static/"):]) if path in ("/agent.json", "/.well-known/agent.json"): diff --git a/static/app.js b/static/app.js deleted file mode 100644 index 2ee48c8..0000000 --- a/static/app.js +++ /dev/null @@ -1,238 +0,0 @@ -// Agent Market frontend: renders UI actions chosen by the agent. Card data goes only to the test payment endpoint. -(() => { - const $ = (s) => document.querySelector(s); - const stage = $("#stage"), log = $("#log"); - const actor = localStorage.getItem("am_actor") || ("guest-" + Math.random().toString(36).slice(2, 7)); - localStorage.setItem("am_actor", actor); - const history = []; - let feeBps = 500; - const compareSet = new Set(); - - const money = (c) => "$" + (c / 100).toFixed(2); - function el(tag, attrs = {}, ...kids) { - const n = document.createElement(tag); - for (const [k, v] of Object.entries(attrs)) { - if (k === "class") n.className = v; - else if (k.startsWith("on")) n.addEventListener(k.slice(2), v); - else if (v !== undefined && v !== null) n.setAttribute(k, v); - } - for (const k of kids.flat()) if (k !== null && k !== undefined) n.append(k.nodeType ? k : document.createTextNode(String(k))); - return n; - } - function toast(text, level = "info") { - const t = el("div", { class: "toast " + level }, text); - $("#toasts").append(t); setTimeout(() => t.remove(), 3500); - } - function say(role, text) { - log.append(el("div", { class: "msg " + role }, text)); log.scrollTop = log.scrollHeight; - } - function openChat() { $("#chat").classList.remove("hidden"); $("#chatLauncher").style.display = "none"; } - - async function api(path, body) { - const r = await fetch(path, body ? { method: "POST", headers: { "Content-Type": "application/json", "X-Agent-Id": actor }, body: JSON.stringify(body) } : { headers: { "X-Agent-Id": actor } }); - return r.json(); - } - - function setModel(model, ms) { - const short = (model || "").replace(/:free$/, ""); - $("#agentBadge").textContent = "AI: " + short; - $("#agentBadge").title = (model || "") + " via OpenRouter (free tier)"; - $("#chatMode").textContent = `AI agent · ${short}${ms ? " · " + (ms / 1000).toFixed(1) + "s" : ""}`; - } - async function talk({ message, event, echo }) { - openChat(); - if (echo) say("user", echo); - const thinking = el("div", { class: "msg sys" }, "agent is thinking…"); log.append(thinking); - try { - const out = await api("/api/chat", { message, event, history: history.slice(-6), actor, theme: AMTheme.get() }); - thinking.remove(); - if (message) history.push({ role: "user", content: message }); - if (event) history.push({ role: "user", content: "UI_EVENT " + JSON.stringify(event) }); - history.push({ role: "assistant", content: out.reply || "" }); - say("bot", out.reply || "(no reply)"); - if (out.agent === "llm") setModel(out.model, out.latency_ms); - else if (out.agent === "fallback") { - $("#chatMode").textContent = "built-in agent answered (AI model unavailable)"; - $("#agentBadge").textContent = "agent: built-in (AI unavailable)"; $("#agentBadge").title = "The hosted model is rate-limited or out of free quota"; - } - else if (out.agent === "rate-limit") toast(out.reply, "warn"); - render(out.actions || []); - } catch (e) { thinking.remove(); say("sys", "Agent error: " + e.message); } - } - - // ---------- component renderers ---------- - const R = { - show_hero(p) { - return el("section", { class: "panel hero" }, el("h2", {}, p.title || "Agent Market"), - el("p", {}, p.subtitle || ""), el("ul", {}, (p.bullets || []).map((b) => el("li", {}, b)))); - }, - show_cards(p) { - return el("section", { class: "panel", "data-component": "cards" }, el("h2", {}, p.heading || "Listings"), - el("div", { class: "by" }, "Rendered by the agent"), - el("div", { class: "grid" }, (p.listings || []).map(card))); - }, - show_compare(p) { - const L = p.listings || [], Q = p.quotes || []; - const row = (label, f) => el("tr", {}, el("th", {}, label), L.map((x, i) => el("td", {}, f(x, Q[i] || {})))); - return el("section", { class: "panel", "data-component": "compare" }, el("h2", {}, p.heading || "Compare"), - el("table", { class: "cmp" }, - el("tr", {}, el("th", {}, ""), L.map((x) => el("th", {}, x.emoji + " " + x.title))), - row("Type", (x) => x.kind), row("Price", (x) => money(x.price)), - row("Pay online (test)", (x, q) => q.online && q.online.available ? `${money(q.online.total_cents)} incl. ${money(q.online.fee_cents)} fee` : "not offered"), - row("Cash in person", (x, q) => q.cash_in_person && q.cash_in_person.available ? `${money(q.cash_in_person.total_cents)} · $0 fee` : "not offered"), - row("Seller", (x) => x.seller + (x.seller_type === "agent" ? " 🤖" : "")), - row("Area", (x) => x.area), row("", (x) => buyButtons(x)))); - }, - show_listing_form(p) { - const d = p.defaults || {}; - const payOpts = Array.isArray(d.payment_options) && d.payment_options.length ? d.payment_options : ["online", "cash"]; - const f = el("form", { class: "form", "data-component": "listing-form" }, - el("label", { class: "full" }, "Title", el("input", { name: "title", required: "", value: d.title || "", placeholder: "e.g. Vintage desk chair" })), - el("label", {}, "Type", (() => { const s = el("select", { name: "kind" }, el("option", { value: "product" }, "Product"), el("option", { value: "service" }, "Service")); s.value = d.kind || "product"; return s; })()), - el("label", {}, "Price (USD)", el("input", { name: "price", type: "number", step: "0.01", min: "0", required: "", value: d.price ? (d.price / 100).toFixed(2) : "" })), - el("label", {}, "Category", el("input", { name: "category", value: d.category || "" })), - el("label", {}, "Area (approximate)", el("input", { name: "area", value: d.area || "Demo Town" })), - el("label", { class: "full" }, "Description", el("textarea", { name: "description", rows: "3", required: "" }, d.description || "")), - el("label", {}, el("span", {}, el("input", { type: "checkbox", name: "online", checked: payOpts.includes("online") ? "" : null }), ` Accept online payment (${feeBps / 100}% fee, paid by buyer)`)), - el("label", {}, el("span", {}, el("input", { type: "checkbox", name: "cash", checked: payOpts.includes("cash") ? "" : null }), " Accept cash in person (free)")), - el("div", { class: "full" }, el("button", { class: "btn", type: "submit" }, "Publish test listing"))); - f.addEventListener("submit", (e) => { - e.preventDefault(); - const fd = new FormData(f); - const fields = { title: fd.get("title"), kind: fd.get("kind"), price: fd.get("price"), category: fd.get("category"), - area: fd.get("area"), description: fd.get("description"), - payment_options: [fd.get("online") && "online", fd.get("cash") && "cash"].filter(Boolean) }; - talk({ event: { type: "submit_listing", fields }, echo: `Publish “${fields.title}”` }); - }); - return el("section", { class: "panel" }, el("h2", {}, "List something"), el("div", { class: "by" }, "Form opened by the agent"), f); - }, - show_checkout(p) { - const it = p.intent, L = p.listing || {}; - const status = el("div", { class: "meta" }); - const card = el("input", { name: "card", value: "4242 4242 4242 4242", inputmode: "numeric", "aria-label": "Test card number" }); - const exp = el("input", { value: "12/34", "aria-label": "Expiry" }), cvc = el("input", { value: "123", "aria-label": "CVC" }); - const payBtn = el("button", { class: "btn", type: "submit" }, `Pay ${money(it.total_cents)} (test)`); - const form = el("form", {}, el("div", { class: "testcard" }, "TEST MODE: no real charge. Success: 4242 4242 4242 4242 · Decline: 4000 0000 0000 0002. Other numbers are refused."), - el("div", { class: "cardform" }, card, exp, cvc), el("div", { style: "margin-top:10px" }, payBtn), status); - form.addEventListener("submit", async (e) => { - e.preventDefault(); payBtn.disabled = true; status.textContent = "Contacting test payment provider…"; - const r = await api(`/api/payments/${it.id}/confirm`, { card_number: card.value }); - status.textContent = r.status === "succeeded" ? "Paid (test)." : "Declined: " + (r.error || "unknown"); - status.className = "meta " + (r.status === "succeeded" ? "ok" : "warn"); - payBtn.disabled = r.status === "succeeded"; - talk({ event: { type: "payment_result", intent_id: it.id, status: r.status, order_id: r.order && r.order.id, error: r.error } }); - }); - return el("section", { class: "panel", "data-component": "checkout" }, el("h2", {}, "Checkout"), - el("div", { class: "by" }, `${L.emoji || ""} ${it.title} · sold by ${L.seller || "?"}`), - el("table", { class: "breakdown" }, - el("tr", {}, el("td", {}, `Price${it.quantity > 1 ? " × " + it.quantity : ""}`), el("td", {}, money(it.subtotal_cents))), - el("tr", {}, el("td", {}, `Facilitation fee (${(it.fee_bps / 100).toFixed(1)}%, online payments only)`), el("td", { "data-testid": "fee" }, money(it.fee_cents))), - el("tr", { class: "total" }, el("td", {}, "You pay"), el("td", { "data-testid": "total" }, money(it.total_cents))), - el("tr", {}, el("td", { class: "meta" }, "Seller receives"), el("td", { class: "meta" }, money(it.subtotal_cents)))), - form, - "cash" in Object.fromEntries((L.payment_options || []).map((x) => [x, 1])) ? - el("p", { class: "meta" }, "Prefer to skip the fee? ", el("a", { href: "#", onclick: (e) => { e.preventDefault(); talk({ event: { type: "buy_cash", listing_id: L.id }, echo: "Pay cash in person instead" }); } }, "Meet and pay cash in person ($0 fee)")) : null); - }, - show_cash_meetup(p) { - const d = p.deal, L = p.listing || {}; - const done = el("button", { class: "btn cash" }, "Mark paid in cash"); - done.addEventListener("click", () => { done.disabled = true; talk({ event: { type: "cash_completed", deal_id: d.id }, echo: "We met and I paid cash" }); }); - return el("section", { class: "panel", "data-component": "cash-meetup" }, el("h2", {}, "Cash in person: no fee"), - el("div", { class: "by" }, `${L.emoji || ""} ${d.title} · seller ${d.seller}`), - el("table", { class: "breakdown" }, - el("tr", {}, el("td", {}, "Price (pay the seller directly)"), el("td", {}, money(d.price_cents))), - el("tr", {}, el("td", {}, "Platform fee"), el("td", { "data-testid": "cash-fee" }, money(d.fee_cents))), - el("tr", { class: "total" }, el("td", {}, "Total"), el("td", {}, money(d.price_cents)))), - el("p", {}, `Suggested meetup: ${d.place} · ${d.time}`), - el("p", { class: "meta" }, "We only connect buyer and seller. Meet somewhere public. (Demo: no real meetup or messaging.)"), - el("div", { class: "meta" }, "Deal " + d.id + " · status " + d.status), el("div", { style: "margin-top:10px" }, done)); - }, - show_receipt(p) { - const o = p.order; - return el("section", { class: "panel", "data-component": "receipt" }, el("h2", { class: "ok" }, o.method === "cash_in_person" ? "Cash deal completed" : "Payment confirmed (test)"), - el("table", { class: "breakdown" }, - el("tr", {}, el("td", {}, o.title), el("td", {}, money(o.subtotal_cents))), - el("tr", {}, el("td", {}, "Facilitation fee"), el("td", { "data-testid": "receipt-fee" }, money(o.fee_cents))), - el("tr", { class: "total" }, el("td", {}, "Total"), el("td", {}, money(o.total_cents))), - el("tr", {}, el("td", { class: "meta" }, "Seller receives"), el("td", { class: "meta" }, money(o.seller_receives_cents)))), - el("div", { class: "meta" }, `Order ${o.id} · ${o.method}${o.card_last4 ? " · test card •••• " + o.card_last4 : ""} · ${o.status}`)); - }, - set_theme(p) { AMTheme.applyAction(p); toast(p.reset && !p.base && !Object.keys(p.patch || {}).length ? "Look reset to default" : "Look updated", "ok"); return null; }, - clear_panel() { stage.innerHTML = ""; return null; }, - toast(p) { toast(p.text || "", p.level || "info"); return null; }, - }; - - function buyButtons(x) { - const opts = x.payment_options || []; - if (x.status !== "active") return el("span", { class: "meta" }, "Sold"); - return el("div", { class: "actions" }, - opts.includes("online") ? el("button", { class: "btn", onclick: () => talk({ event: { type: "buy_online", listing_id: x.id }, echo: `Buy “${x.title}” online` }) }, "Buy online") : null, - opts.includes("cash") ? el("button", { class: "btn cash", onclick: () => talk({ event: { type: "buy_cash", listing_id: x.id }, echo: `Meet & pay cash for “${x.title}”` }) }, "Cash meetup") : null); - } - function card(x) { - const cb = el("input", { type: "checkbox", "aria-label": "compare" }); - cb.checked = compareSet.has(x.id); - cb.addEventListener("change", () => { cb.checked ? compareSet.add(x.id) : compareSet.delete(x.id); updateCompareBar(); }); - return el("div", { class: "card" + (x.status !== "active" ? " sold" : ""), "data-listing": x.id }, - el("div", { class: "emoji" }, x.emoji || "📦"), el("h3", {}, x.title), - el("div", { class: "price" }, money(x.price), el("span", { class: "meta" }, x.kind === "service" ? " / session" : "")), - el("div", {}, el("span", { class: "tag" }, x.kind), x.seller_type === "agent" ? el("span", { class: "tag agent-tag" }, "sold by an agent") : null, - el("span", { class: "tag" }, "test listing")), - el("div", { class: "meta" }, x.area + " · " + x.seller), - el("label", { class: "meta" }, cb, " compare"), buyButtons(x)); - } - const cmpbar = el("div", { class: "cmpbar" }); document.body.append(cmpbar); - function updateCompareBar() { - cmpbar.innerHTML = ""; cmpbar.style.display = compareSet.size ? "flex" : "none"; - cmpbar.append(`${compareSet.size} selected`, el("button", { class: "btn", onclick: () => talk({ event: { type: "compare", listing_ids: [...compareSet] }, echo: "Compare selected" }) }, "Compare")); - } - - function render(actions) { - let first = true; - for (const a of actions) { - const fn = R[a.type]; if (!fn) continue; - const node = fn(a.payload || {}); - if (!node) continue; - if (first) { stage.innerHTML = ""; first = false; if (AMTheme.ensureResultsVisible()) toast("Results section shown again so you can see this", "info"); } - stage.append(node); - } - if (!first) stage.scrollIntoView({ behavior: "smooth", block: "start" }); - } - - // ---------- wiring ---------- - document.addEventListener("click", (e) => { - const b = e.target.closest("[data-say],[data-event]"); if (!b) return; - if (b.dataset.say) talk({ message: b.dataset.say, echo: b.dataset.say }); - else talk({ event: { type: b.dataset.event }, echo: b.textContent.trim() }); - }); - $("#chatLauncher").addEventListener("click", openChat); - $("#chatClose").addEventListener("click", () => { $("#chat").classList.add("hidden"); $("#chatLauncher").style.display = ""; }); - $("#chatForm").addEventListener("submit", (e) => { - e.preventDefault(); const v = $("#chatInput").value.trim(); if (!v) return; - $("#chatInput").value = ""; talk({ message: v, echo: v }); - }); - - const copyDefaults = (bps) => ({ - logo: "Agent Market", headline: "A marketplace you can just talk to.", - subhead: "Tell the agent what you want. It searches, compares, lists your stuff and runs the checkout, right here on the page. You can also restyle the whole page from the chat.", - chips: [{ label: "🚲 Show me bikes", say: "show me bikes" }, { label: "📐 Tutoring", say: "find tutoring services" }, - { label: "⚖️ Compare tutoring", say: "compare the two tutoring options" }, { label: "➕ Sell something", event: "sell" }, - { label: "🎨 Make it look retro", say: "make it look retro" }], - bullets: [`${(bps / 100).toFixed(bps % 100 ? 1 : 0)}% facilitation fee when you pay online (test mode)`, "$0 fee when you meet and pay cash in person", - "Agents welcome: other AI agents buy and sell through the open API (see For agents)"], - }); - AMTheme.init(copyDefaults(feeBps)); - $("#resetLook").addEventListener("click", () => { AMTheme.reset(); toast("Look reset to default", "ok"); }); - api("/api/status").then((s) => { - feeBps = s.fee_bps; AMTheme.init(copyDefaults(feeBps)); - if (s.agent.mode === "llm" && s.agent.daily_quota_exhausted_until_utc) { $("#agentBadge").textContent = "agent: built-in (AI quota used up)"; $("#agentBadge").title = "Hosted model free quota resets " + s.agent.daily_quota_exhausted_until_utc; } - else if (s.agent.mode === "llm") setModel(s.agent.last_model || s.agent.model); - else { $("#agentBadge").textContent = "agent: rule-based fallback"; $("#agentBadge").title = s.agent.note || ""; } - }); - // Landing: show featured listings via the agent. - (async () => { - const out = await api("/api/chat", { event: { type: "landing" }, actor }); - render(out.actions || []); - say("bot", "Hi! I run this page. Ask me to find, compare, sell, or buy. Payments are test mode only."); - })(); -})(); diff --git a/static/index.html b/static/index.html deleted file mode 100644 index 126ab40..0000000 --- a/static/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - -Agent Market (demo) - - - - - -
TEST DEMO: all listings are fake and payments run in test mode. No real money moves.
-
-
Agent Market
- -
- -
-
-

A marketplace you can just talk to.

-

Tell the agent what you want. It searches, compares, lists your stuff and runs the checkout, right here on the page. You can also restyle the whole page from the chat.

-
-
- -
-
Agent Market demo · fake test data · test-mode payments · open API
-
- - - -
- - - - diff --git a/static/theme.js b/static/theme.js deleted file mode 100644 index 4c6ef6c..0000000 --- a/static/theme.js +++ /dev/null @@ -1,174 +0,0 @@ -// Per-visitor theme engine. The theme is a validated JSON spec of design tokens stored in localStorage -// (never on the server), so one visitor's restyle never changes the page for anyone else. -(() => { - const KEY = "am_theme_v1"; - const FONTS = { - system: ["system-ui,-apple-system,Segoe UI,Roboto,sans-serif"], serif: ["Georgia,'Times New Roman',serif"], mono: ["ui-monospace,Menlo,Consolas,monospace"], - Inter: ["'Inter',system-ui,sans-serif", "Inter:wght@400;600;700"], Poppins: ["'Poppins',system-ui,sans-serif", "Poppins:wght@400;600;700"], - Montserrat: ["'Montserrat',system-ui,sans-serif", "Montserrat:wght@400;600;700"], Nunito: ["'Nunito',system-ui,sans-serif", "Nunito:wght@400;700"], - "Space Grotesk": ["'Space Grotesk',system-ui,sans-serif", "Space+Grotesk:wght@400;700"], Lora: ["'Lora',Georgia,serif", "Lora:wght@400;700"], - Merriweather: ["'Merriweather',Georgia,serif", "Merriweather:wght@400;700"], "Playfair Display": ["'Playfair Display',Georgia,serif", "Playfair+Display:wght@400;700"], - "Roboto Slab": ["'Roboto Slab',Georgia,serif", "Roboto+Slab:wght@400;700"], "Fira Code": ["'Fira Code',ui-monospace,monospace", "Fira+Code:wght@400;600"], - "Space Mono": ["'Space Mono',ui-monospace,monospace", "Space+Mono:wght@400;700"], VT323: ["'VT323',ui-monospace,monospace", "VT323"], - "Press Start 2P": ["'Press Start 2P',ui-monospace,monospace", "Press+Start+2P"], "Bebas Neue": ["'Bebas Neue',Impact,sans-serif", "Bebas+Neue"], - Pacifico: ["'Pacifico',cursive", "Pacifico"], "Comic Neue": ["'Comic Neue','Comic Sans MS',cursive", "Comic+Neue:wght@400;700"], - }; - const MODES = { - dark: { bg: "#0f1115", surface: "#171a21", card: "#1d212a", text: "#e9ecf1", muted: "#9aa3b2", accent: "#7c9cff", border: "#2a2f3a" }, - light: { bg: "#f5f6fa", surface: "#ffffff", card: "#ffffff", text: "#151821", muted: "#5b6475", accent: "#3d5afe", border: "#dde1ea" }, - }; - const SECTIONS = ["hero", "chips", "bullets", "results", "footer"]; - const IMAGE_HOSTS = ["images.unsplash.com", "picsum.photos", "upload.wikimedia.org"]; - const isObj = (o) => o && typeof o === "object" && !Array.isArray(o); - const merge = (a, b) => { const o = { ...(a || {}) }; for (const [k, v] of Object.entries(b || {})) o[k] = isObj(v) && isObj(o[k]) ? merge(o[k], v) : v; return o; }; - const okColor = (c) => typeof c === "string" && c.length < 60 && !/[;{}<>]/.test(c) && CSS.supports("color", c); - - // ---- colour maths for guaranteed contrast on protected elements ---- - const probe = document.createElement("span"); - function rgb(c) { - probe.style.color = ""; probe.style.color = c; document.body.append(probe); - const m = getComputedStyle(probe).color.match(/[\d.]+/g) || [0, 0, 0, 1]; probe.remove(); - return { r: +m[0], g: +m[1], b: +m[2], a: m[3] === undefined ? 1 : +m[3] }; - } - const lum = ({ r, g, b }) => [r, g, b].map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }) - .reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0); - const contrast = (a, b) => { const x = lum(rgb(a)), y = lum(rgb(b)); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); }; - const solid = (c) => { const v = rgb(c); return `rgb(${v.r},${v.g},${v.b})`; }; - const bestInk = (bg) => (contrast(bg, "#111111") >= contrast(bg, "#ffffff") ? "#111111" : "#ffffff"); - window.__amContrast = contrast; - - function backgroundCss(bg, pal) { - const cols = (bg.colors || []).filter(okColor); - const c0 = cols[0] || pal.bg, c1 = cols[1] || pal.surface; - switch (bg.kind) { - case "gradient": return `linear-gradient(${Number(bg.angle ?? 135)}deg, ${(cols.length > 1 ? cols : [c0, c1]).join(", ")})`; - case "pattern": { - const p = bg.pattern || "dots"; - const map = { - dots: `radial-gradient(${c1} 1.5px, transparent 1.6px) 0 0/18px 18px, ${c0}`, - grid: `linear-gradient(${c1} 1px, transparent 1px) 0 0/24px 24px, linear-gradient(90deg, ${c1} 1px, transparent 1px) 0 0/24px 24px, ${c0}`, - stripes: `repeating-linear-gradient(90deg, ${c0} 0 18px, ${c1} 18px 36px)`, - diagonal: `repeating-linear-gradient(45deg, ${c0} 0 14px, ${c1} 14px 28px)`, - checks: `conic-gradient(${c1} 25%, ${c0} 0 50%, ${c1} 0 75%, ${c0} 0) 0 0/40px 40px`, - waves: `radial-gradient(circle at 50% 0, transparent 18px, ${c1} 19px 21px, transparent 22px) 0 0/40px 24px, ${c0}`, - }; - return map[p] || map.dots; - } - case "image": { - try { const u = new URL(bg.url); if (u.protocol === "https:" && IMAGE_HOSTS.includes(u.hostname)) return `center/cover no-repeat url("${u.href.replace(/["\\()]/g, "")}"), ${c0}`; } catch (e) {} - return c0; - } - default: return c0; - } - } - - function loadFont(name) { - const f = FONTS[name]; if (!f) return FONTS.system[0]; - if (f[1] && !document.querySelector(`link[data-font="${name}"]`)) { - const l = document.createElement("link"); l.rel = "stylesheet"; l.dataset.font = name; - l.href = `https://fonts.googleapis.com/css2?family=${f[1]}&display=swap`; document.head.append(l); - } - return f[0]; - } - - function scopeCss(css) { - if (typeof css !== "string" || /[<\\@]|expression\s*\(|javascript:|!\s*important|--[\w-]+\s*:/i.test(css)) return ""; - const urlOk = [...css.matchAll(/url\(([^)]*)\)/gi)].every((m) => { try { return IMAGE_HOSTS.includes(new URL(m[1].replace(/["']/g, "").trim()).hostname); } catch (e) { return false; } }); - if (!urlOk) return ""; - return [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map(([, sel, body]) => - sel.split(",").map((s) => "#page " + s.trim()).join(", ") + "{" + body + "}").join("\n"); - } - - let theme = {}; - try { theme = JSON.parse(localStorage.getItem(KEY) || "{}") || {}; } catch (e) { theme = {}; } - const defaults = { copy: {} }; - - function apply() { - const t = theme, root = document.documentElement.style, body = document.body; - const mode = t.mode === "light" ? "light" : "dark"; - const pal = { ...MODES[mode] }; - for (const [k, v] of Object.entries(t.palette || {})) if (k in pal && okColor(v)) pal[k] = v; - const set = (k, v) => root.setProperty(k, v); - set("--bg", pal.bg); set("--panel", pal.surface); set("--card", pal.card); set("--ink", pal.text); - set("--mute", pal.muted); set("--acc", pal.accent); set("--line", pal.border); set("--acc-ink", bestInk(pal.accent)); - set("--page-bg", backgroundCss(isObj(t.background) ? t.background : { kind: "solid", colors: [pal.bg] }, pal)); - set("--font", loadFont(FONTS[t.font] ? t.font : "system")); - set("--hfont", FONTS[t.headingFont] ? loadFont(t.headingFont) : "var(--font)"); - set("--scale", String(Math.min(1.75, Math.max(0.75, Number(t.fontScale) || 1)))); - set("--radius", Math.min(32, Math.max(0, Number(t.radius ?? 14))) + "px"); - set("--space", String({ compact: 0.65, normal: 1, comfy: 1.4 }[t.density] || 1)); - const cols = Math.min(6, Math.max(0, parseInt(t.columns) || 0)); - set("--grid-cols", cols ? `repeat(${cols}, minmax(0, 1fr))` : "repeat(auto-fill, minmax(220px, 1fr))"); - // text that sits directly on the page background (header, hero, bullets, footer) must stay readable on - // gradients/patterns/images too: keep the theme colour only if it clears 4.5:1 against every background stop - const stops = (isObj(t.background) && Array.isArray(t.background.colors) ? t.background.colors.filter(okColor) : []); - const bgStops = stops.length ? stops.map(solid) : [solid(pal.bg)]; - const minC = (ink) => Math.min(...bgStops.map((b) => contrast(ink, b))); - let pageInk = minC(pal.text) >= 4.5 ? pal.text : (minC("#111111") >= minC("#ffffff") ? "#111111" : "#ffffff"); - // busy gradients (light and dark stops) can't be fixed by text colour alone: put a readability scrim behind page text - const scrim = minC(pageInk) < 4.5; - body.classList.toggle("scrim", scrim); - if (scrim) { set("--scrim", solid(pal.surface)); pageInk = bestInk(solid(pal.surface)); } - set("--page-ink", pageInk); set("--page-mute", !scrim && minC(pal.muted) >= 4.5 ? pal.muted : pageInk); - // protected panels: solid surface colour + whichever of black/white reads best (always >= 4.5:1) - const pbg = solid(pal.surface); set("--p-bg", pbg); set("--p-ink", bestInk(pbg)); - // chat popup - const ch = isObj(t.chat) ? t.chat : {}; - const cbg = okColor(ch.bg) ? ch.bg : pal.surface, cacc = okColor(ch.accent) ? ch.accent : pal.accent; - set("--chat-bg", solid(cbg)); set("--chat-ink", okColor(ch.text) && contrast(ch.text, solid(cbg)) >= 4.5 ? ch.text : bestInk(solid(cbg))); - set("--chat-acc", cacc); set("--chat-acc-ink", bestInk(cacc)); - set("--chat-w", Math.min(600, Math.max(280, Number(ch.width) || 370)) + "px"); - set("--chat-h", Math.min(820, Math.max(320, Number(ch.height) || 520)) + "px"); - body.classList.toggle("chat-left", ch.position === "left"); - for (const s of ["flat", "outlined", "elevated", "glass"]) body.classList.toggle("card-" + s, t.cardStyle === s); - body.classList.toggle("layout-list", t.layout === "list"); - body.classList.toggle("no-anim", t.animations === false); - // copy - const c = { ...defaults.copy, ...(isObj(t.copy) ? t.copy : {}) }; - document.getElementById("logoText").textContent = String(c.logo || "Agent Market").slice(0, 40); - document.getElementById("headline").textContent = String(c.headline).slice(0, 120); - document.getElementById("subhead").textContent = String(c.subhead).slice(0, 240); - const chips = document.getElementById("chips"); chips.innerHTML = ""; - for (const chip of (Array.isArray(c.chips) ? c.chips : []).slice(0, 6)) { - const b = document.createElement("button"); b.className = "chip"; - if (chip.event) b.dataset.event = chip.event; else b.dataset.say = String(chip.say || chip.label).slice(0, 120); - b.textContent = String(chip.label).slice(0, 40); chips.append(b); - } - const ul = document.getElementById("bullets"); ul.innerHTML = ""; - for (const line of (Array.isArray(c.bullets) ? c.bullets : []).slice(0, 6)) { const li = document.createElement("li"); li.textContent = String(line).slice(0, 140); ul.append(li); } - if (t.copy && t.copy.footer) document.getElementById("footer").textContent = String(t.copy.footer).slice(0, 160); - else if (defaults.footerHtml) document.getElementById("footer").innerHTML = defaults.footerHtml; - // sections - const sec = isObj(t.sections) ? t.sections : {}; - const order = Array.isArray(sec.order) ? sec.order.filter((x) => SECTIONS.includes(x)) : []; - const full = [...new Set([...order, ...SECTIONS])]; - const hidden = Array.isArray(sec.hidden) ? sec.hidden : []; - for (const name of SECTIONS) { - const n = document.querySelector(`[data-section="${name}"]`); - n.style.order = String(full.indexOf(name)); - n.classList.toggle("is-hidden", hidden.includes(name)); - } - document.getElementById("user-css").textContent = t.customCss ? "@layer user {\n" + scopeCss(t.customCss) + "\n}" : ""; - } - - window.AMTheme = { - init(copyDefaults) { defaults.copy = copyDefaults; defaults.footerHtml = document.getElementById("footer").innerHTML; apply(); }, - get: () => theme, - // action from the agent: {reset?, base?, patch} - applyAction(a) { - if (a.reset) theme = {}; - if (a.base) theme = JSON.parse(JSON.stringify(a.base)); - theme = merge(theme, a.patch || {}); - localStorage.setItem(KEY, JSON.stringify(theme)); apply(); - }, - reset() { theme = {}; localStorage.removeItem(KEY); apply(); }, - // a panel that must be visible (checkout etc.) un-hides the results section - ensureResultsVisible() { - if (theme.sections && Array.isArray(theme.sections.hidden) && theme.sections.hidden.includes("results")) { - theme.sections.hidden = theme.sections.hidden.filter((x) => x !== "results"); localStorage.setItem(KEY, JSON.stringify(theme)); apply(); return true; - } - return false; - }, - contrast, - }; -})(); diff --git a/tests/e2e_http.py b/tests/e2e_http.py index 869ff79..37da037 100755 --- a/tests/e2e_http.py +++ b/tests/e2e_http.py @@ -28,7 +28,12 @@ def check(name, cond, detail=""): s, html = call("GET", "/", raw=True) -check("landing page loads", s == 200 and "Agent Market" in html and "chatLauncher" in html) +import re +check("landing page loads (React SPA shell)", s == 200 and "Agent Market" in html and '
' in html) +assets = re.findall(r'(?:src|href)="(/static/dist/assets/[^"]+)"', html) +js = [a for a in assets if a.endswith(".js")] +bundle = call("GET", js[0], raw=True) if js else (0, "") +check("built JS bundle served", bundle[0] == 200 and "chatLauncher" in bundle[1] and "show_checkout" in bundle[1], f"assets={len(assets)}") s, st = call("GET", "/api/status") check("status endpoint", s == 200 and st["payments"]["mode"] == "test", f"agent={st['agent']['mode']} fee_bps={st['fee_bps']}") fee_bps = st["fee_bps"] diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..80264c2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + +Agent Market (demo) + + + + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..bf6d428 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1722 @@ +{ + "name": "agent-market-web", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-market-web", + "version": "0.2.0", + "dependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^5.2.0", + "typescript": "5.9", + "vite": "^7.3.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.2.tgz", + "integrity": "sha512-gjNDUwMswogkw3fNqV00u54uZlgmInuw5EU/1TeqQ6K0bEagoGvqjPdKzy112lKyhe7PkGudZ/kSQHsc9qroIg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.2.tgz", + "integrity": "sha512-YkAW8q7p5SCLSaGWaUkmsRnBEb3MGA1pB5nI/8k0lAK5fZsQKRBqkdR1h4UU9p8u3c7ZGdUF2Zcx7mK+E/pUKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.2.tgz", + "integrity": "sha512-hNmuIHv+LnrP/+q/44vsmIMWfoh7X2YXOp5FLtjGuvqN52VdtFpOpnshTc1k28tWv1wE/0D5gLjsozBUowEGbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.2.tgz", + "integrity": "sha512-B0t5CoxgF2KL7R5FHAsyN8yVEQCSIkh4XVs8MCDjuBLNjEa8l3zd5tVKaFMv53LfUtHs9p2rnqeavPTOunV+Ww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.2.tgz", + "integrity": "sha512-QQdYnfwrPmc1DoPlddjFvQPzUHFbStKXsHrG6EBYsWHhBVUCjsCRdcfGGXGAvhDpDtTzKK4vy1UrGFJ45rQaiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.2.tgz", + "integrity": "sha512-1xRxtGWif6BB58VSfMavi0Yfs2M4x6CtRp4o7ScY4W7AGlgvf6GSPKdDNdw7TX3tMiQwZY5W/mvyrOJTq/7AKA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.2.tgz", + "integrity": "sha512-CXFLEt384BUwwOLR9DURWNHl1bQOVLd1ibOee+yKBJm/fZtvrSAOZ0JbxjAxszVMIovYMwFMS+oP+Q1aPcx8Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.2.tgz", + "integrity": "sha512-T1dorY0WHm5mMNpMHFl5rYRZ02gLSZcr6J45r66tYb3S3R3DyIRGiGWVdLtF+zOQjratNJaObl9jYumueRiL1A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.2.tgz", + "integrity": "sha512-ia6bfaCIc0ldp6bvs9oEkFrJVJKNUMoiDF0+LBZsQAZg+vE0znXnzGqH+ew/FYswE30cruAWPTO3dPw7keaVkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.2.tgz", + "integrity": "sha512-mB/gwfBfXtT0EBpbSssqKCqMFuX8b21Zy2TGoIhehSJEssZ5ksybxymwp1BkEM5Nczh/AAxpmh5q+MkhtundGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.2.tgz", + "integrity": "sha512-VPd0a+XlHoNRI9jIPRHww2UKhDkD6XjbtcKYXZijAnhJS1jLLbiNtVl6a5LyEfNN+eyiyWcJOZXPzBo69oTsew==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.2.tgz", + "integrity": "sha512-10N1I6oBpERqzGtHKcCLrEpHlbXSBbMK9TURHvqgWbIFZuU3oJROxJ0H2nHfq96TVI0XJHQ9qzolJMFd2tz0LA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.2.tgz", + "integrity": "sha512-dhJrLVZ3wNNhwCmy9/VTg6Pq6J8a8eZ0n15dVRlbSWcDb+8V/1mDfbnvis+vt8W6Pbxzv6gUbTEl4piOT3bbUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.2.tgz", + "integrity": "sha512-tPd2QuwxkTxgju5eARyb3RdgBWWi/O9dn/NFH0VkvGbLzoQeXrH6qMABtW8IODb5lUeBzB3sy2U7qmI4MtQ72g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.2.tgz", + "integrity": "sha512-njtDdPvSQWI9CDhIPPxsiEMeHayCfu7wwcbSQ7c2kYPhGVviL+WGVhq6Jfj/b+VuhSTkFuQ0z4ZFbFjkWwBFmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.2.tgz", + "integrity": "sha512-EbOBo3dqCj9ecEV9Z7zvulB6AieMP2ztnKQYGVOSN0pzIyOu+3JH2YJZnLmbPeVvfn6yP6Qi7NbBM27EzGbj2w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.2.tgz", + "integrity": "sha512-8wLt2Q5n4qmfiE2Gk8V6KNLSf+oJuBX7Kdw5joFDtnZIZvirS0hLzBc79OiJjV71oYDmBUZLNy6pDgvylo4qiQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.2.tgz", + "integrity": "sha512-aIChbkhC4d33bxkVP0LSi2/2lUSGRXE6k4Qa0G7tgpgTOK7kxHTixmx5qC3SKUJ6yUMznHBtQoWaG4QMqLUnhw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.2.tgz", + "integrity": "sha512-gQ+TkVZRfjZVcl/UOAxxfWw+sHjzIXoieNwxEyJlD3tRBmBhR8ZDq11w+QEgQDcpslEB+fhN/OEpaTsDoIIHhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.2.tgz", + "integrity": "sha512-sUaJpMKS2JBCvUk8fYimG7aMq2jHVT4kw/uVHxPolKLrMmkKy0KXirmZyZehdNpL7PffqAlPAWUELQcTiD8ChA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.2.tgz", + "integrity": "sha512-j1RdJWb0CrlSuwt12OPjLucPlwfLtA/KMOKVvh5rXSJEtl/WDoIGR3CSykK/jLGTo9RqNfW5Ub6kY5Z5ccYl9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.2.tgz", + "integrity": "sha512-i3r04aHCIY3eggTBLUp/4IxiBjDpAgQaK89Ue2nMJ2MKuIXX5JfoeZjHw+kPjGGNVDlhi8FT8xzuGMJZe3aehg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.2.tgz", + "integrity": "sha512-kCQ15h/uwNMGdN8gjXBZGf+aGE+ODzHfbi90/cAm4+Q89WoMiLODNoj8HoGZmYx87HPxfu5CBNPsZ+ZG4uIAoA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.2.tgz", + "integrity": "sha512-yYAuySSs+0X2gSMJPw/Q4jHsHZHyMq1dpAE+XnN790Jg1WuReUAD/+j+e4vuDvS4PXJ4PA4NCzTR4OYtDf18/g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.2.tgz", + "integrity": "sha512-JY1rQdWOJrKutORqKO1uzrHXV86jFiQzfLFKx2s8S1CjLXL9fC5LOIvk0q7Z7PXJvsByt+sdoE1Q2a9gBr4gLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.451", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.451.tgz", + "integrity": "sha512-vUNKPrUzw8ByEMOxloq0a71iD/mPuPxVkbDm3G5YhnxTzOw/I6qAg2MUS0cYj/c267lWmw26yW4Vv7foxTxPLg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.2.tgz", + "integrity": "sha512-RxXm0XVVV1hFI3dYe12ruaOgsYQ725lUcUWeMSQqkF9tTxVErY+fy2Ets6vWhItL9MZMSNZGbvZFc7rH/VHp2Q==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.2", + "@rollup/rollup-android-arm64": "4.64.2", + "@rollup/rollup-darwin-arm64": "4.64.2", + "@rollup/rollup-darwin-x64": "4.64.2", + "@rollup/rollup-freebsd-arm64": "4.64.2", + "@rollup/rollup-freebsd-x64": "4.64.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.2", + "@rollup/rollup-linux-arm-musleabihf": "4.64.2", + "@rollup/rollup-linux-arm64-gnu": "4.64.2", + "@rollup/rollup-linux-arm64-musl": "4.64.2", + "@rollup/rollup-linux-loong64-gnu": "4.64.2", + "@rollup/rollup-linux-loong64-musl": "4.64.2", + "@rollup/rollup-linux-ppc64-gnu": "4.64.2", + "@rollup/rollup-linux-ppc64-musl": "4.64.2", + "@rollup/rollup-linux-riscv64-gnu": "4.64.2", + "@rollup/rollup-linux-riscv64-musl": "4.64.2", + "@rollup/rollup-linux-s390x-gnu": "4.64.2", + "@rollup/rollup-linux-x64-gnu": "4.64.2", + "@rollup/rollup-linux-x64-musl": "4.64.2", + "@rollup/rollup-openbsd-x64": "4.64.2", + "@rollup/rollup-openharmony-arm64": "4.64.2", + "@rollup/rollup-win32-arm64-msvc": "4.64.2", + "@rollup/rollup-win32-ia32-msvc": "4.64.2", + "@rollup/rollup-win32-x64-gnu": "4.64.2", + "@rollup/rollup-win32-x64-msvc": "4.64.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..5667bc5 --- /dev/null +++ b/web/package.json @@ -0,0 +1,22 @@ +{ + "name": "agent-market-web", + "private": true, + "version": "0.2.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^5.2.0", + "typescript": "5.9", + "vite": "^7.3.7" + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..d3d4490 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { actor, api } from "./api"; +import { Ctx, type AgentCtx, type Talk } from "./context"; +import { EFFECTS, PANELS } from "./components/registry"; +import { Chat, type Msg } from "./components/Chat"; +import { + applyThemeToDocument, contrast, loadTheme, reduceThemeAction, resolveCopy, resolveSections, saveTheme, + type Copy, type Section, type Theme, type ThemeAction, +} from "./theme/engine"; +import type { ChatReply, Status, UIAction } from "./types"; + +declare global { interface Window { __amContrast: typeof contrast } } +window.__amContrast = contrast; // used by the guardrail tests + +const copyDefaults = (bps: number): Copy => ({ + logo: "Agent Market", headline: "A marketplace you can just talk to.", + subhead: "Tell the agent what you want. It searches, compares, lists your stuff and runs the checkout, right here on the page. You can also restyle the whole page from the chat.", + chips: [{ label: "🚲 Show me bikes", say: "show me bikes" }, { label: "📐 Tutoring", say: "find tutoring services" }, + { label: "⚖️ Compare tutoring", say: "compare the two tutoring options" }, { label: "➕ Sell something", event: "sell" }, + { label: "🎨 Make it look retro", say: "make it look retro" }], + bullets: [`${(bps / 100).toFixed(bps % 100 ? 1 : 0)}% facilitation fee when you pay online (test mode)`, "$0 fee when you meet and pay cash in person", + "Agents welcome: other AI agents buy and sell through the open API (see For agents)"], +}); + +interface Panel { key: string; type: string; payload: any } +interface Toast { id: number; text: string; level: string } +let seq = 0; + +export default function App() { + const [theme, setTheme] = useState(loadTheme); + const themeRef = useRef(theme); themeRef.current = theme; + const [feeBps, setFeeBps] = useState(500); + const [panels, setPanels] = useState([]); + const [msgs, setMsgs] = useState([]); + const [chatOpen, setChatOpen] = useState(false); + const [chatMode, setChatMode] = useState("AI agent"); + const [badge, setBadge] = useState({ text: "agent: …", title: "" }); + const [toasts, setToasts] = useState([]); + const [compareIds, setCompareIds] = useState([]); + const history = useRef<{ role: string; content: string }[]>([]); + const stageRef = useRef(null); + + // Theme: tokens/classes/customCss go onto the document; copy + sections are rendered below. + useLayoutEffect(() => { applyThemeToDocument(theme); saveTheme(theme); }, [theme]); + const resetTheme = useCallback(() => { themeRef.current = {}; setTheme({}); }, []); + + const toast = useCallback((text: string, level: "info" | "ok" | "warn" = "info") => { + const id = ++seq; setToasts((ts) => [...ts, { id, text, level }]); + setTimeout(() => setToasts((ts) => ts.filter((t) => t.id !== id)), 3500); + }, []); + const say = useCallback((role: Msg["role"], text: string) => { const id = ++seq; setMsgs((m) => [...m, { id, role, text }]); return id; }, []); + const unsay = useCallback((id: number) => setMsgs((m) => m.filter((x) => x.id !== id)), []); + const setModel = useCallback((model?: string, ms?: number) => { + const short = (model || "").replace(/:free$/, ""); + setBadge({ text: "AI: " + short, title: (model || "") + " via OpenRouter (free tier)" }); + setChatMode(`AI agent · ${short}${ms ? " · " + (ms / 1000).toFixed(1) + "s" : ""}`); + }, []); + + // Render agent actions through the registry: effects run, panels replace the results area. + const render = useCallback((actions: UIAction[]) => { + // Theme changes in this batch are folded locally so a panel later in the same batch sees them + // (e.g. a set_theme that hides "results" followed by a checkout un-hides it again, like before). + let t = themeRef.current; + const fx = { toast, applyTheme: (a: ThemeAction) => { t = reduceThemeAction(t, a); }, clearStage: () => setPanels([]) }; + const batch: Panel[] = []; + for (const a of actions) { + if (EFFECTS[a.type]) EFFECTS[a.type](a.payload || {}, fx); + else if (PANELS[a.type]) batch.push({ key: `${++seq}`, type: a.type, payload: a.payload || {} }); + } + if (batch.length) { + const hidden = t.sections?.hidden; + if (Array.isArray(hidden) && hidden.includes("results")) { + t = { ...t, sections: { ...t.sections, hidden: hidden.filter((x: string) => x !== "results") } }; + toast("Results section shown again so you can see this", "info"); + } + } + if (t !== themeRef.current) { themeRef.current = t; setTheme(t); } + if (!batch.length) return; + setPanels(batch); + setTimeout(() => stageRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), 0); + }, [toast]); + + const talk = useCallback(async ({ message, event, echo }: Talk) => { + setChatOpen(true); + if (echo) say("user", echo); + const thinking = say("sys", "agent is thinking…"); + try { + const out = await api("/api/chat", { message, event, history: history.current.slice(-6), actor, theme: themeRef.current }); + unsay(thinking); + if (message) history.current.push({ role: "user", content: message }); + if (event) history.current.push({ role: "user", content: "UI_EVENT " + JSON.stringify(event) }); + history.current.push({ role: "assistant", content: out.reply || "" }); + say("bot", out.reply || "(no reply)"); + if (out.agent === "llm") setModel(out.model, out.latency_ms); + else if (out.agent === "fallback") { + setChatMode("built-in agent answered (AI model unavailable)"); + setBadge({ text: "agent: built-in (AI unavailable)", title: "The hosted model is rate-limited or out of free quota" }); + } else if (out.agent === "rate-limit") toast(out.reply || "", "warn"); + render(out.actions || []); + } catch (e) { unsay(thinking); say("sys", "Agent error: " + (e as Error).message); } + }, [say, unsay, setModel, toast, render]); + + // Boot: status (fee + badge), then the deterministic landing view and greeting. + useEffect(() => { + api("/api/status").then((s) => { + setFeeBps(s.fee_bps); + if (s.agent.mode === "llm" && s.agent.daily_quota_exhausted_until_utc) + setBadge({ text: "agent: built-in (AI quota used up)", title: "Hosted model free quota resets " + s.agent.daily_quota_exhausted_until_utc }); + else if (s.agent.mode === "llm") setModel(s.agent.last_model || s.agent.model); + else setBadge({ text: "agent: rule-based fallback", title: s.agent.note || "" }); + }); + api("/api/chat", { event: { type: "landing" }, actor }).then((out) => { + render(out.actions || []); + say("bot", "Hi! I run this page. Ask me to find, compare, sell, or buy. Payments are test mode only."); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const ctx: AgentCtx = useMemo(() => ({ + talk, feeBps, toast, + compare: { + has: (id) => compareIds.includes(id), + toggle: (id, on) => setCompareIds((ids) => (on ? [...new Set([...ids, id])] : ids.filter((x) => x !== id))), + }, + }), [talk, feeBps, toast, compareIds]); + + const copy = resolveCopy(theme, copyDefaults(feeBps)); + const { order, hidden } = resolveSections(theme); + const sec = (s: Section) => ({ "data-section": s, style: { order: order[s] }, className: hidden.has(s) ? "is-hidden" : undefined }); + const cls = (base: string, s: Section) => [base, hidden.has(s) ? "is-hidden" : ""].filter(Boolean).join(" "); + + return ( + + {/* Protected: the test banner, the agent badge and the checkout/fee panels can't be restyled by themes. */} +
TEST DEMO: all listings are fake and payments run in test mode. No real money moves.
+
+
◆ {copy.logo}
+ +
+ +
+
+

{copy.headline}

+

{copy.subhead}

+
+
+ {copy.chips.map((c, i) => ( + + ))} +
+
    + {copy.bullets.map((b, i) =>
  • {b.slice(0, 140)}
  • )} +
+
+ {panels.map((p) => { const C = PANELS[p.type]; return ; })} +
+
+ {copy.footer || <>Agent Market demo · fake test data · test-mode payments · open API} +
+
+ + talk({ message: v, echo: v })} /> +
{toasts.map((t) =>
{t.text}
)}
+
+ {compareIds.length} selected + +
+
+ ); +} diff --git a/web/src/api.ts b/web/src/api.ts new file mode 100644 index 0000000..92be041 --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,13 @@ +// Thin fetch wrapper. X-Agent-Id identifies this browser's demo actor (same as the pre-React app). +export const actor: string = (() => { + const a = localStorage.getItem("am_actor") || "guest-" + Math.random().toString(36).slice(2, 7); + localStorage.setItem("am_actor", a); + return a; +})(); + +export async function api(path: string, body?: unknown): Promise { + const r = await fetch(path, body !== undefined + ? { method: "POST", headers: { "Content-Type": "application/json", "X-Agent-Id": actor }, body: JSON.stringify(body) } + : { headers: { "X-Agent-Id": actor } }); + return r.json(); +} diff --git a/web/src/components/Chat.tsx b/web/src/components/Chat.tsx new file mode 100644 index 0000000..5105d86 --- /dev/null +++ b/web/src/components/Chat.tsx @@ -0,0 +1,31 @@ +import { useEffect, useRef, useState } from "react"; + +export interface Msg { id: number; role: "user" | "bot" | "sys"; text: string } + +/** Chat popup. Typed messages go to the model via /api/chat; position/size/colours come from the theme. */ +export function Chat({ open, setOpen, mode, msgs, onSend }: { + open: boolean; setOpen: (o: boolean) => void; mode: string; msgs: Msg[]; onSend: (text: string) => void; +}) { + const [value, setValue] = useState(""); + const logRef = useRef(null); + useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [msgs, open]); + return ( + <> + + + + ); +} diff --git a/web/src/components/ListingForm.tsx b/web/src/components/ListingForm.tsx new file mode 100644 index 0000000..4e2fe2e --- /dev/null +++ b/web/src/components/ListingForm.tsx @@ -0,0 +1,37 @@ +import { useAgent } from "../context"; + +interface Defaults { title?: string; kind?: string; price?: number; category?: string; area?: string; description?: string; payment_options?: string[] } + +/** show_listing_form — prefilled by the agent; Publish sends a deterministic submit_listing event. */ +export function ListingForm({ payload }: { payload: { defaults?: Defaults } }) { + const { talk, feeBps } = useAgent(); + const d = payload.defaults || {}; + const pay = Array.isArray(d.payment_options) && d.payment_options.length ? d.payment_options : ["online", "cash"]; + const submit = (e: React.FormEvent) => { + e.preventDefault(); + const fd = new FormData(e.currentTarget); + const fields = { + title: fd.get("title"), kind: fd.get("kind"), price: fd.get("price"), category: fd.get("category"), + area: fd.get("area"), description: fd.get("description"), + payment_options: [fd.get("online") && "online", fd.get("cash") && "cash"].filter(Boolean), + }; + talk({ event: { type: "submit_listing", fields }, echo: `Publish “${fields.title}”` }); + }; + return ( +
+

List something

+
Form opened by the agent
+
+ + + + + +