Spaces:
Running
Running
| import { type ClassValue, clsx } from "clsx"; | |
| import { twMerge } from "tailwind-merge"; | |
| export function cn(...inputs: ClassValue[]) { | |
| return twMerge(clsx(inputs)); | |
| } | |
| export function shortAddr(addr?: string | null, head = 6, tail = 4): string { | |
| if (!addr) return "—"; | |
| if (addr.length <= head + tail + 2) return addr; | |
| return `${addr.slice(0, head)}…${addr.slice(-tail)}`; | |
| } | |
| /** | |
| * Format a reputation score for display. | |
| * | |
| * Backend stores reputation as a raw decimal in [0, 1]. The UI surfaces it | |
| * uniformly as a whole-number percent (e.g. ``0.85`` → ``"85%"``) so the | |
| * leaderboard win-rate column and the bid/operator rep columns share one | |
| * convention. Pass ``rawDecimal=true`` only when the surrounding text is a | |
| * formula (e.g. ``max(reputation, 1.0)`` in the auction explainer) where the | |
| * 0–1 scale is mathematically required. | |
| * | |
| * NOTE (W14-D): The on-chain `ReputationRegistry.sol` EMA has a known unit- | |
| * scale bug — `_fillSignal` stays pinned at the 0.5 floor for any realistic | |
| * fee, so this value is currently *not* informative as a primary UX metric. | |
| * Operator/leaderboard surfaces have switched to `formatWinsBids()` as the | |
| * primary display; this helper still drives the auction-explainer formula | |
| * and any "advanced / on-chain raw" detail panels. | |
| */ | |
| export function formatReputation( | |
| value: number | null | undefined, | |
| options?: { rawDecimal?: boolean }, | |
| ): string { | |
| if (value === null || value === undefined || Number.isNaN(value)) return "—"; | |
| if (options?.rawDecimal) return value.toFixed(2); | |
| return `${Math.round(value * 100)}%`; | |
| } | |
| /** | |
| * Format a wins-over-bids ratio for display (W14-D primary UX metric). | |
| * | |
| * Returns a string like ``"12/47 · 26%"`` for `wins=12, totalBids=47`. | |
| * Falls back to ``"—"`` when either input is missing/NaN. When totalBids | |
| * is 0 we render ``"0/0"`` without a percent (no auctions entered). | |
| * | |
| * This is the demo-safe replacement for the on-chain EMA reputation badge: | |
| * the EMA suffers from a known `_fillSignal` unit-scale bug in | |
| * `ReputationRegistry.sol` (stuck at the 0.5 floor), so we lead with the | |
| * unambiguous, off-chain wins/bids count and relegate the raw EMA to an | |
| * "advanced" detail row with an explainer tooltip. | |
| */ | |
| export function formatWinsBids( | |
| wins: number | null | undefined, | |
| totalBids: number | null | undefined, | |
| ): string { | |
| if ( | |
| wins === null || | |
| wins === undefined || | |
| Number.isNaN(wins) || | |
| totalBids === null || | |
| totalBids === undefined || | |
| Number.isNaN(totalBids) | |
| ) { | |
| return "—"; | |
| } | |
| if (totalBids === 0) return "0/0"; | |
| const pct = Math.round((wins / totalBids) * 100); | |
| return `${wins}/${totalBids} · ${pct}%`; | |
| } | |
| export function formatUsd(value: number | null | undefined, fractionDigits = 2): string { | |
| if (value === null || value === undefined || Number.isNaN(value)) return "—"; | |
| return new Intl.NumberFormat("en-US", { | |
| style: "currency", | |
| currency: "USD", | |
| minimumFractionDigits: fractionDigits, | |
| maximumFractionDigits: fractionDigits, | |
| }).format(value); | |
| } | |
| export function formatNumber(value: number | null | undefined, fractionDigits = 2): string { | |
| if (value === null || value === undefined || Number.isNaN(value)) return "—"; | |
| return new Intl.NumberFormat("en-US", { | |
| minimumFractionDigits: fractionDigits, | |
| maximumFractionDigits: fractionDigits, | |
| }).format(value); | |
| } | |
| /** | |
| * Determine whether a raw `anchor.ipfsCid` / pipeline_trace_ipfs / reasoning_ipfs | |
| * value is a *real* IPFS content identifier we can route through a public gateway, | |
| * vs. a synthetic/mock provenance path emitted by the backend (e.g. | |
| * `ipfs://pipeline/qwen/59fac6348e57`). The latter must NOT render as a clickable | |
| * gateway link because the gateway will 404. | |
| * | |
| * Real CIDs: | |
| * - v0: `Qm` + 44 base58 chars | |
| * - v1: `bafy` + 55 base32 chars (most common; other multibase prefixes exist | |
| * but in this codebase the live anchor flow emits `bafy…`) | |
| * | |
| * Anything else — including `ipfs://pipeline/...`, `ipfs://mock/...`, | |
| * `ipfs://synthetic/...`, or a bare placeholder string — should be displayed as | |
| * a muted, non-clickable provenance label. | |
| * | |
| * Returns `{ cid, isReal, gatewayUrl }` where: | |
| * - `cid` is the input with any `ipfs://` scheme prefix stripped | |
| * - `isReal` true if `cid` matches the v0/v1 CID shape above | |
| * - `gatewayUrl` populated only when `isReal` is true | |
| */ | |
| export function classifyIpfsRef(raw?: string | null): { | |
| cid: string; | |
| isReal: boolean; | |
| gatewayUrl?: string; | |
| } | null { | |
| if (!raw) return null; | |
| const cid = raw.replace(/^ipfs:\/\//, ""); | |
| if (!cid) return null; | |
| const v0 = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/; | |
| const v1 = /^bafy[a-zA-Z0-9]{55,}$/; | |
| // A bare CID has no path separators. Synthetic refs like | |
| // `pipeline/qwen/abc` contain slashes and must be rejected. | |
| const isReal = !cid.includes("/") && (v0.test(cid) || v1.test(cid)); | |
| return { | |
| cid, | |
| isReal, | |
| gatewayUrl: isReal ? `https://ipfs.io/ipfs/${cid}` : undefined, | |
| }; | |
| } | |
| /** | |
| * Detect a synthetic ("sim-prefix") Arc tx hash emitted by the backend in | |
| * mock mode (W5-A2). Real Arc testnet tx hashes are `0x` + 64 hex chars; | |
| * synthetic mock hashes always begin with the literal prefix `0xsim_`. | |
| * | |
| * The UI uses this gate at every external-explorer link site so a synthetic | |
| * hash is rendered as muted, non-clickable text rather than wrapped in an | |
| * `https://testnet.arcscan.app/tx/0xsim_…` link that would 404. | |
| */ | |
| export function isSimTxHash(h: string | null | undefined): boolean { | |
| return typeof h === "string" && h.toLowerCase().startsWith("0xsim_"); | |
| } | |
| /** | |
| * Detect a synthetic Polymarket market_id emitted by the backend in mock / | |
| * dry-run mode. The backend uses two prefixes: `sim-` for fully simulated | |
| * markets and `dryrun-` for dry-run submissions. Either prefix means the | |
| * market does not exist on polymarket.com and the UI MUST NOT wrap it in an | |
| * external link. | |
| */ | |
| export function isSimPolymarketId(id: string | null | undefined): boolean { | |
| if (typeof id !== "string") return false; | |
| const lower = id.toLowerCase(); | |
| return lower.startsWith("sim-") || lower.startsWith("dryrun-"); | |
| } | |
| /** | |
| * Build a Polymarket market URL when, and only when, the supplied id is a | |
| * real (non-sim) market_id. Returns `null` for sim / dry-run ids so callers | |
| * can fall back to a muted, non-clickable text label. | |
| */ | |
| export function polymarketMarketUrl(id: string | null | undefined): string | null { | |
| if (!id || isSimPolymarketId(id)) return null; | |
| return `https://polymarket.com/market/${id}`; | |
| } | |
| /** | |
| * Validate a `market_url` field returned by the backend. The backend may | |
| * synthesise a `polymarket.com/market/sim-...` URL in mock mode; we don't | |
| * trust it blindly. Returns the URL only when it's a real polymarket URL | |
| * (i.e. the market_id segment does NOT start with `sim-` / `dryrun-`); for | |
| * any sim-prefixed URL, returns `null` so the UI renders muted text. | |
| */ | |
| export function safePolymarketUrl(url: string | null | undefined): string | null { | |
| if (typeof url !== "string" || !url) return null; | |
| const match = url.match(/\/market\/([^/?#]+)/); | |
| if (match && isSimPolymarketId(match[1])) return null; | |
| return url; | |
| } | |
| /** | |
| * Build an Arc testnet explorer URL for a tx hash when, and only when, the | |
| * hash is a *real* on-chain hash. Synthetic `0xsim_…` hashes return `null` | |
| * so callers can render a muted span instead of a broken external link. | |
| * | |
| * The base URL is the canonical Arc testnet explorer used by `arcTxUrl()` | |
| * in `ui/lib/api.ts`; we don't import from there to avoid a cycle and to | |
| * keep this helper self-contained for unit testing. | |
| */ | |
| export function arcscanTxUrl(h: string | null | undefined): string | null { | |
| if (!h || isSimTxHash(h)) return null; | |
| return `https://testnet.arcscan.app/tx/${h}`; | |
| } | |
| export function relativeTime(iso?: string | null): string { | |
| if (!iso) return "—"; | |
| const then = new Date(iso).getTime(); | |
| if (Number.isNaN(then)) return iso; | |
| const diffSec = Math.round((Date.now() - then) / 1000); | |
| if (diffSec < 60) return `${diffSec}s ago`; | |
| if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; | |
| if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; | |
| return `${Math.floor(diffSec / 86400)}d ago`; | |
| } | |