Spaces:
Running
Running
File size: 8,260 Bytes
88d2f2a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | 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`;
}
|