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/README.md b/README.md index f386a0a..270a3b3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ sell products or services. ## How it works ``` -browser (static HTML/JS) ──/api/chat──▶ tiny server ──▶ hosted LLM (OpenAI-compatible) +browser (React SPA) ─────/api/chat──▶ tiny server ──▶ hosted LLM (OpenAI-compatible) ▲ renders UI actions │ or the rule-based fallback agent │ ├── /api/payments/* ── mock test-mode provider ── signed webhook └── card form posts straight ────────┘ @@ -38,7 +38,7 @@ The server stays small on purpose. It does three things: ### Look and layout (agent-controlled, per visitor) The agent can restyle the whole page from chat with one action, `set_theme`. Its payload is a JSON theme spec: design tokens applied as CSS variables. The model never sends raw HTML or JS. The server checks every value -(`market/theme.py`), and the browser applies the result (`static/theme.js`). +(`market/theme.py`), and the browser applies the result (`web/src/theme/engine.ts`). | Area | Spec keys | |---|---| @@ -115,12 +115,76 @@ The landing view doesn't call the model either. Set `AGENT_MARKET_LLM_EVENTS=1` With no LLM configured, the demo runs the **rule-based fallback agent**. The page labels this. +## Frontend (React + Vite + TypeScript) + +The page is a React single-page app in `web/`. The Python server and API are unchanged, except that `/` now serves +the built `static/dist/index.html` and `/static/dist/assets/*` serves the hashed JS/CSS bundles. + +``` +web/ + index.html # declares `@layer base, user, protected;` first, then - - - -
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.

-
-
- -
- -
- - - -
- - - - 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/browser_e2e.js b/tests/browser_e2e.js index bb2d09c..0390896 100644 --- a/tests/browser_e2e.js +++ b/tests/browser_e2e.js @@ -15,6 +15,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await page.goto(BASE + "/", { waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); check("landing renders agent-chosen cards", (await page.$$(".card")).length >= 4); + check("React app mounted in #root", await page.evaluate(() => !!document.querySelector("#root > .testbar") && !!document.querySelector("#root main#page #stage [data-component=cards]"))); const badge = await page.$eval("#agentBadge", (n) => n.textContent); check("agent mode badge shown", /fallback|built-in|AI:|LLM/.test(badge), badge); @@ -82,6 +83,21 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await page.waitForSelector("[data-component=compare]", { timeout: 15000 }); check("compare view renders", (await page.$$("[data-component=compare] th")).length >= 3); + // compare via card checkboxes + compare bar + await ask("show me everything"); + await page.$$eval("#stage .card input[aria-label=compare]", (ns) => ns.slice(0, 2).forEach((n) => n.click())); + const bar = await page.$eval(".cmpbar", (n) => ({ t: n.textContent, d: getComputedStyle(n).display })); + await page.click(".cmpbar button"); + await page.waitForFunction(() => document.querySelectorAll("[data-component=compare] tr:first-child th").length === 3, { timeout: 15000 }).catch(() => {}); + check("compare bar: 2 selected -> compare view", bar.d === "flex" && /^2 selected/.test(bar.t) && (await page.$$("[data-component=compare] tr:first-child th")).length === 3, bar.t); + + // chat popup close / reopen + await page.click("#chatClose"); + const closed = await page.evaluate(() => document.getElementById("chat").classList.contains("hidden") && getComputedStyle(document.getElementById("chatLauncher")).display !== "none"); + await page.click("#chatLauncher"); + const reopened = await page.evaluate(() => !document.getElementById("chat").classList.contains("hidden") && document.querySelectorAll("#log .msg").length > 5); + check("chat popup closes and reopens with history", closed && reopened); + check("no page JS errors", errors.length === 0, errors.join("; ")); await browser.close(); const passed = results.filter(Boolean).length; 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/tests/fake_model_browser.js b/tests/fake_model_browser.js new file mode 100644 index 0000000..0f6fb17 --- /dev/null +++ b/tests/fake_model_browser.js @@ -0,0 +1,67 @@ +// React UI against the fake OpenAI-compatible model (tests/mock_llm.py): model badge + chat mode text, fallback badge +// texts, quota badge, rate-limit toast, and a model-sent set_theme that the server sanitised. No real model calls. +// Usage: NODE_PATH= node tests/fake_model_browser.js +const puppeteer = require("puppeteer-core"); +const [OK, BUSY, DAILY, RL] = process.argv.slice(2, 6); +const results = []; +const check = (name, ok, d = "") => { results.push(!!ok); console.log((ok ? "PASS " : "FAIL ") + name + (d ? ` [${d}]` : "")); }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +(async () => { + const browser = await puppeteer.launch({ executablePath: process.env.CHROME || "/usr/bin/google-chrome", headless: "new", + args: ["--no-sandbox", "--disable-gpu", "--user-data-dir=/tmp/am-fake-model-" + Date.now()] }); + const errors = []; + const open = async (base) => { + const ctx = await browser.createBrowserContext(); const page = await ctx.newPage(); + await page.setViewport({ width: 1280, height: 860 }); page.on("pageerror", (e) => errors.push(e.message)); + await page.goto(base + "/", { waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); + return page; + }; + const ask = async (page, text) => { + const n = (await page.$$(".msg.bot")).length; + if (await page.$eval("#chat", (c) => c.classList.contains("hidden"))) await page.click("#chatLauncher"); + await page.type("#chatInput", text); await page.keyboard.press("Enter"); + await page.waitForFunction((n) => document.querySelectorAll(".msg.bot").length > n, { timeout: 30000 }, n); + await sleep(300); + return page.$$eval(".msg.bot", (ns) => ns[ns.length - 1].textContent); + }; + const text = (page, sel) => page.$eval(sel, (n) => n.textContent); + + // 1) model answers: badge + chat mode show the model; tool-hydrated cards render; unknown component ignored + let p = await open(OK); + check("badge shows configured model on load", (await text(p, "#agentBadge")) === "AI: mock-model", await text(p, "#agentBadge")); + await ask(p, "show me bikes"); + const mode = await text(p, "#chatMode"), badge = await text(p, "#agentBadge"), title = await p.$eval("#agentBadge", (n) => n.title); + check("model reply: badge + chat mode", badge === "AI: mock-model" && /^AI agent · mock-model · \d+\.\ds$/.test(mode) && title === "mock-model via OpenRouter (free tier)", `${badge} | ${mode} | ${title}`); + const titles = await p.$$eval("#stage .card h3", (ns) => ns.map((n) => n.textContent)); + check("model-chosen cards rendered through the registry", titles.length > 0 && titles.every((t) => /bike/i.test(t)) && (await p.$$("#stage > section")).length === 1, titles.join(" | ")); + // 2) model restyle: server-sanitised set_theme applied; HTML in copy rendered as text; customCss scoped + const reply = await ask(p, "make the background a sunset gradient"); + const st = await p.evaluate(() => ({ bg: getComputedStyle(document.body).backgroundImage, img: !!document.querySelector("#headline img"), + h: document.getElementById("headline").textContent, css: document.getElementById("user-css").textContent, + toast: [...document.querySelectorAll("#toasts .toast")].map((t) => t.textContent).join("|"), ls: JSON.parse(localStorage.getItem("am_theme_v1") || "{}") })); + check("model set_theme applied (gradient, toast, saved)", /gradient/.test(st.bg) && /Look updated/.test(st.toast) && st.ls.background, st.bg.slice(0, 60)); + check("model copy/CSS sanitised in the page", !st.img && /Sunset/.test(st.h) && !/ built-in agent answers, fallback badge text + p = await open(BUSY); + await ask(p, "show me bikes"); + check("fallback badge + chat mode text", (await text(p, "#agentBadge")) === "agent: built-in (AI unavailable)" && (await text(p, "#chatMode")) === "built-in agent answered (AI model unavailable)" && (await p.$$("#stage .card")).length > 0, + `${await text(p, "#agentBadge")} | ${await text(p, "#chatMode")}`); + // 4) daily quota used up -> after reload the badge says so (from /api/status) + p = await open(DAILY); + await ask(p, "show me bikes"); + await p.reload({ waitUntil: "networkidle0" }); await sleep(300); + check("quota badge after reload", (await text(p, "#agentBadge")) === "agent: built-in (AI quota used up)" && /resets/.test(await p.$eval("#agentBadge", (n) => n.title)), await text(p, "#agentBadge")); + // 5) per-IP rate limit -> warning toast + p = await open(RL); + await ask(p, "show me bikes"); await ask(p, "show me bikes"); + const toasts = await p.$$eval("#toasts .toast.warn", (ns) => ns.map((n) => n.textContent)); + check("rate-limit warning toast", toasts.some((t) => /quickly/.test(t)), toasts.join("|")); + + check("no page JS errors", errors.length === 0, errors.join("; ")); + await browser.close(); + const passed = results.filter(Boolean).length; + console.log(`\n${passed}/${results.length} passed`); + process.exit(passed === results.length ? 0 : 1); +})().catch((e) => { console.error(e); process.exit(2); }); diff --git a/tests/fake_model_browser.sh b/tests/fake_model_browser.sh new file mode 100755 index 0000000..c07543c --- /dev/null +++ b/tests/fake_model_browser.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Browser checks of the React UI against the fake model (no real key, no real model calls). Needs a built frontend +# (cd web && npm ci && npm run build) and puppeteer-core on NODE_PATH. +set -euo pipefail +cd "$(dirname "$0")/.." +[ -f static/dist/index.html ] || { echo "build the frontend first (cd web && npm run build)"; exit 1; } +D=$(mktemp -d); KEYF=$D/key; printf 'test-dummy-key' > $KEYF; chmod 600 $KEYF +python3 tests/mock_llm.py 18803 & M=$! +start() { # port models rpm burst + AGENT_MARKET_PORT=$1 AGENT_MARKET_DATA=$D/d$1 AGENT_MARKET_LLM_API_KEY_FILE=$KEYF AGENT_MARKET_CHAT_RPM=$3 AGENT_MARKET_CHAT_BURST=$4 \ + AGENT_MARKET_LLM_BASE_URL=http://127.0.0.1:18803/v1 AGENT_MARKET_LLM_MODELS=$2 python3 server.py >/dev/null 2>&1 & echo $!; } +S1=$(start 18804 mock-model 0 6); S2=$(start 18805 busy-model,busy-model-2 0 6); S3=$(start 18806 daily-model 0 6); S4=$(start 18807 mock-model 1 1) +trap 'kill $M $S1 $S2 $S3 $S4 2>/dev/null; rm -rf $D' EXIT +sleep 1.5 +node tests/fake_model_browser.js http://127.0.0.1:18804 http://127.0.0.1:18805 http://127.0.0.1:18806 http://127.0.0.1:18807 diff --git a/tests/theme_check.js b/tests/theme_check.js index 31e34e3..bdd9f28 100644 --- a/tests/theme_check.js +++ b/tests/theme_check.js @@ -109,6 +109,11 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const h = await page.$eval("#headline", (n) => n.textContent); check("headline changed", /buy and sell anything nearby/i.test(h), h); + // The look is stored per browser: a reload keeps it. + await page.reload({ waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); + check("look persists across reload (localStorage)", /buy and sell anything nearby/i.test(await page.$eval("#headline", (n) => n.textContent)) && (await cs("#bullets", "display")) === "none"); + await page.click("#chatLauncher"); + // A second, fresh session must still see the default look. const ctx2 = await browser.createBrowserContext(); const p2 = await ctx2.newPage(); await p2.goto(BASE + "/", { waitUntil: "networkidle0" }); @@ -120,6 +125,22 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const t5 = await theme(); const h2 = await page.$eval("#headline", (n) => n.textContent); check("reset the look", Object.keys(t5).length === 0 && /just talk to/.test(h2), JSON.stringify(t5)); + // A hostile stored theme (tampered localStorage) can't hide or recolour protected parts, and a hidden results + // section is shown again when the agent renders a panel. Then the "Reset look" button restores the default. + await page.evaluate(() => localStorage.setItem("am_theme_v1", JSON.stringify({ + palette: { bg: "#ffffff", surface: "#fefefe", text: "#fdfdfd", muted: "#fefefe", accent: "#ffff00" }, sections: { hidden: ["results"] }, + customCss: ".testbar{display:none!important;opacity:0} #agentBadge{visibility:hidden;color:#fff} [data-component=checkout] *{color:#fff!important;background:#fff} body{--p-ink:#fff} .card{border-width:3px}" }))); + await page.reload({ waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); await sleep(300); + const hv = await page.evaluate(() => ({ stage: getComputedStyle(document.getElementById("stage")).display, css: document.getElementById("user-css").textContent, + toast: [...document.querySelectorAll("#toasts .toast")].map((t) => t.textContent).join("|"), hidden: (JSON.parse(localStorage.getItem("am_theme_v1") || "{}").sections || {}).hidden })); + check("hidden results shown again for agent panels", hv.stage !== "none" && /Results section shown again/.test(hv.toast) && !hv.hidden.includes("results"), hv.toast); + check("tampered stored customCss dropped by the client (fail-closed)", /^@layer user/.test(hv.css) && !/testbar|important|--p-ink|agentBadge/.test(hv.css), hv.css.replace(/\s+/g, " ").slice(0, 120)); + await guardrails("hostile stored theme"); + await page.click("#resetLook"); await sleep(300); + const rs = await page.evaluate(() => ({ ls: localStorage.getItem("am_theme_v1"), h: document.getElementById("headline").textContent, css: document.getElementById("user-css").textContent, + toast: [...document.querySelectorAll("#toasts .toast")].map((t) => t.textContent).join("|") })); + check("Reset look button restores the default", rs.ls === null && /just talk to/.test(rs.h) && rs.css === "" && /Look reset to default/.test(rs.toast), JSON.stringify(rs).slice(0, 120)); + check("no internal ids in replies", replies.every((x) => !/\b(lst|pi|cash|ord)_[0-9a-f]{6,}/.test(x.reply))); const who = [...new Set(replies.map((x) => (/AI agent/.test(x.mode) ? x.mode.split(" · ")[1] : "built-in agent")))].join(", "); if (process.env.REQUIRE_LLM) check("all restyles answered by the real model", replies.every((x) => /AI agent/.test(x.mode)), who); 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
+
+ + + + + +