Spaces:
Running
Running
Download web/src/api.ts from nsaroiu/mtg-deck-completion: direct link, hf CLI and curl.
- Browser
- Download file 4.98 kB
-
https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/web/src/api.ts
- Command line
-
hf download hf://spaces/nsaroiu/mtg-deck-completion/web/src/api.ts
-
curl -L -o api.ts https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/web/src/api.ts
4.98 kB
| // Typed client for the FastAPI server (server/app.py). | |
| export type CardSource = 'input' | 'synergy' | 'staple' | 'basic' | |
| /** Real-deck-frequency classification, independent of `source` -- the | |
| * synergy phase is unrestricted and routinely picks real staples on their | |
| * own merit, so a card can be source: 'synergy' and tier: 'staple' at | |
| * once. null for basics (outside the tokenizer vocab, so untiered). */ | |
| export type CardTier = 'staple' | 'mid' | 'long_tail' | |
| /** What it takes to show a card: its row (cost) and hover preview (text, | |
| * stats, price). Deck cards and search results both carry it. */ | |
| export interface CardInfo { | |
| name: string | |
| type_line: string | |
| mana_cost: string | |
| cmc: number | |
| category: string | |
| oracle_text: string | |
| tier: CardTier | null | |
| /** Per-face power/toughness, loyalty, or defense, e.g. ["4/3", null]. */ | |
| stats: (string | null)[] | |
| /** Cheapest paper printing, if Scryfall has a price for it. */ | |
| price: CardPrice | null | |
| color_identity: string[] | |
| /** A deck may hold more than one copy (basics, Relentless Rats, ...). */ | |
| multiples_allowed: boolean | |
| } | |
| export interface DeckCard extends CardInfo { | |
| quantity: number | |
| source: CardSource | |
| score: number | null | |
| locked: boolean | |
| } | |
| export interface CardPrice { | |
| usd: number | |
| set: string | |
| finish: 'nonfoil' | 'foil' | 'etched' | |
| } | |
| export interface Completion { | |
| commanders: DeckCard[] | |
| cards: DeckCard[] | |
| warnings: string[] | |
| summary: { | |
| total: number | |
| lands: number | |
| target_lands: number | |
| added: number | |
| basics_added: number | |
| /** Sum of the cheapest printing of every card, commanders included. */ | |
| price_usd: number | |
| /** Cards (by quantity) with no known price, left out of price_usd. */ | |
| unpriced: number | |
| } | |
| /** When the price data was built (ISO time), or null if there is none yet. */ | |
| prices_updated: string | null | |
| } | |
| /** A search result: a full CardInfo, so it can be shown anywhere a deck card is. */ | |
| export interface CardSummary extends CardInfo { | |
| banned: boolean | |
| } | |
| export interface SearchOptions { | |
| commanderOnly?: boolean | |
| /** Only cards within this color identity, e.g. "BG" ("" = colorless). */ | |
| colors?: string | |
| /** Also offer basic lands (they're not otherwise searchable). */ | |
| basics?: boolean | |
| } | |
| export interface UnresolvedCard { | |
| name: string | |
| line: number | null // null = a commander field, not a decklist line | |
| suggestions: string[] | |
| } | |
| export interface LockedCard { | |
| name: string | |
| quantity: number | |
| source: CardSource | |
| /** False: kept by this request without being locked (your basics, or the | |
| * rest of a deck over 100), so it can be cut. Default true. */ | |
| locked?: boolean | |
| } | |
| export interface CompleteRequest { | |
| commanders: string[] | |
| decklist: string | |
| target_lands: number | null | |
| prune: boolean | |
| /** Cards kept from the current deck when rerolling. */ | |
| locked?: LockedCard[] | |
| /** Cards the model must never suggest (the user's ban list). */ | |
| exclude?: string[] | |
| } | |
| export class ApiError extends Error { | |
| unresolved: UnresolvedCard[] | |
| constructor(message: string, unresolved: UnresolvedCard[] = []) { | |
| super(message) | |
| this.unresolved = unresolved | |
| } | |
| } | |
| async function parseError(res: Response): Promise<ApiError> { | |
| try { | |
| const body = await res.json() | |
| if (typeof body.error === 'string') return new ApiError(body.error, body.unresolved ?? []) | |
| // FastAPI request-validation errors: {detail: [{msg, loc}, ...]} | |
| if (Array.isArray(body.detail)) return new ApiError(body.detail.map((d: { msg: string }) => d.msg).join('; ')) | |
| } catch { | |
| // fall through | |
| } | |
| return new ApiError(`Request failed (${res.status}).`) | |
| } | |
| export async function completeDeck(req: CompleteRequest, signal?: AbortSignal): Promise<Completion> { | |
| const res = await fetch('/api/complete', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(req), | |
| signal, | |
| }) | |
| if (!res.ok) throw await parseError(res) | |
| return res.json() | |
| } | |
| export interface Config { | |
| default_target_lands: number | |
| min_target_lands: number | |
| /** When the price data was built (ISO time), or null if there is none yet. */ | |
| prices_updated: string | null | |
| } | |
| export async function getConfig(signal?: AbortSignal): Promise<Config> { | |
| const res = await fetch('/api/config', { signal }) | |
| if (!res.ok) throw await parseError(res) | |
| return res.json() | |
| } | |
| export async function searchCards(q: string, options: SearchOptions = {}, signal?: AbortSignal): Promise<CardSummary[]> { | |
| const params = new URLSearchParams({ q, commander: String(options.commanderOnly ?? false), limit: '8' }) | |
| if (options.colors !== undefined) params.set('colors', options.colors) | |
| if (options.basics) params.set('basics', 'true') | |
| const res = await fetch(`/api/cards?${params}`, { signal }) | |
| if (!res.ok) throw await parseError(res) | |
| return (await res.json()).cards | |
| } | |
| export function scryfallPageUrl(name: string): string { | |
| return `https://scryfall.com/search?q=${encodeURIComponent(`!"${name}"`)}` | |
| } | |