Download frontend/src/api.ts from Duke410/PawTrace: direct link, hf CLI and curl.
- Browser
- Download file 9.5 kB
-
https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/frontend/src/api.ts
- Command line
-
hf download hf://spaces/Duke410/PawTrace/frontend/src/api.ts
-
curl -L -o api.ts https://huggingface.co/spaces/Duke410/PawTrace/resolve/main/frontend/src/api.ts
9.5 kB
| // Thin typed fetch wrapper. Token is injected from localStorage (JWT auth — DECISIONS.md). | |
| import type { | |
| AdminCaseDetail, | |
| AdminCasesPage, | |
| CaseDog, | |
| AdminDog, | |
| AdminDogsPage, | |
| AdminOwnerDetail, | |
| AdminOwnersPage, | |
| BreedOption, | |
| BreedEstimateResult, | |
| DogDetailResult, | |
| PhotoSearchResult, | |
| TestMatchResult, | |
| Case, | |
| Dataset, | |
| DatasetDetail, | |
| DatasetDogsPage, | |
| EmbedAllResult, | |
| FoundReportResponse, | |
| Job, | |
| KnownDog, | |
| Match, | |
| MatchDatasetResult, | |
| Page, | |
| PurgeResult, | |
| ReclaimInfo, | |
| User, | |
| } from "./types"; | |
| const TOKEN_KEY = "pawtrace_token"; | |
| export function getToken(): string | null { | |
| return localStorage.getItem(TOKEN_KEY); | |
| } | |
| export function setToken(t: string | null) { | |
| if (t) localStorage.setItem(TOKEN_KEY, t); | |
| else localStorage.removeItem(TOKEN_KEY); | |
| } | |
| export class ApiError extends Error { | |
| status: number; | |
| constructor(status: number, message: string) { | |
| super(message); | |
| this.status = status; | |
| } | |
| } | |
| async function request<T>(path: string, opts: RequestInit = {}): Promise<T> { | |
| const headers = new Headers(opts.headers); | |
| const token = getToken(); | |
| if (token) headers.set("Authorization", `Bearer ${token}`); | |
| if (!(opts.body instanceof FormData) && opts.body) { | |
| headers.set("Content-Type", "application/json"); | |
| } | |
| const res = await fetch(path, { ...opts, headers }); | |
| if (res.status === 204) return undefined as T; | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) { | |
| const msg = data?.error?.message || data?.detail || res.statusText; | |
| throw new ApiError(res.status, msg); | |
| } | |
| return data as T; | |
| } | |
| export const api = { | |
| // auth | |
| register: (body: { name: string; email: string; password: string; zip: string; phone?: string }) => | |
| request<{ access_token: string }>("/auth/register", { method: "POST", body: JSON.stringify(body) }), | |
| login: (body: { email: string; password: string }) => | |
| request<{ access_token: string }>("/auth/login", { method: "POST", body: JSON.stringify(body) }), | |
| me: () => request<User>("/auth/me"), | |
| // dogs | |
| listDogs: () => request<Page<KnownDog>>("/dogs"), | |
| getDog: (id: number) => request<KnownDog>(`/dogs/${id}`), | |
| createDog: (body: Partial<KnownDog>) => | |
| request<KnownDog>("/dogs", { method: "POST", body: JSON.stringify(body) }), | |
| uploadDogPhotos: (id: number, files: File[]) => { | |
| const fd = new FormData(); | |
| files.forEach((f) => fd.append("files", f)); | |
| return request<unknown>(`/dogs/${id}/photos`, { method: "POST", body: fd }); | |
| }, | |
| markDogHome: (id: number) => request<KnownDog>(`/dogs/${id}/mark-home`, { method: "POST" }), | |
| // cases | |
| createLost: (body: Record<string, unknown>) => | |
| request<FoundReportResponse>("/cases/lost", { method: "POST", body: JSON.stringify(body) }), | |
| createFound: (fd: FormData) => | |
| request<FoundReportResponse>("/cases/found", { method: "POST", body: fd }), | |
| listCases: () => request<Page<Case>>("/cases"), | |
| getCase: (id: number) => request<Case>(`/cases/${id}`), | |
| getCaseMatches: (id: number) => request<Match[]>(`/cases/${id}/matches`), | |
| getCaseDog: (id: number) => request<CaseDog>(`/cases/${id}/dog`), | |
| widen: (id: number) => request<FoundReportResponse>(`/cases/${id}/widen`, { method: "POST" }), | |
| widenBreed: (id: number) => | |
| request<FoundReportResponse>(`/cases/${id}/widen-breed`, { method: "POST" }), | |
| rematchCase: (id: number) => | |
| request<FoundReportResponse>(`/cases/${id}/rematch`, { method: "POST" }), | |
| updateCase: (id: number, body: { notes?: string; close?: boolean }) => | |
| request<Case>(`/cases/${id}`, { method: "PATCH", body: JSON.stringify(body) }), | |
| // matches | |
| confirmMatch: (id: number) => request<Match>(`/matches/${id}/confirm`, { method: "POST" }), | |
| rejectMatch: (id: number) => request<Match>(`/matches/${id}/reject`, { method: "POST" }), | |
| reconsiderMatch: (id: number) => request<Match>(`/matches/${id}/reconsider`, { method: "POST" }), | |
| reclaimInfo: (id: number) => request<ReclaimInfo>(`/matches/${id}/reclaim`), | |
| // geo | |
| shelters: (zip?: string) => | |
| request<{ zip: string | null; shelters: { name: string; detail: string }[] }>( | |
| `/geo/nearby-shelters${zip ? `?zip=${encodeURIComponent(zip)}` : ""}` | |
| ), | |
| // public runtime config (read on load to decide which UI to render + haystack size for the UI) | |
| getConfig: () => request<{ demo_mode: boolean; haystack_size: number }>("/config"), | |
| // public photo search (no auth). Accepts 1-6 images of the same dog — the server scores every | |
| // query image against every candidate image and keeps the best pair, so more angles only help. | |
| // pool: "found" searches found/unknown dogs (I lost my dog); "lost" searches known/lost dogs (I | |
| // found a dog and want its owner). | |
| searchByPhoto: (files: File[], zip?: string, pool: "found" | "lost" = "found", topK = 12) => { | |
| const fd = new FormData(); | |
| files.forEach((f) => fd.append("files", f)); | |
| if (zip) fd.append("zip", zip); | |
| fd.append("top_k", String(topK)); | |
| fd.append("pool", pool); | |
| return request<PhotoSearchResult>("/search/by-photo", { method: "POST", body: fd }); | |
| }, | |
| // Breed estimation over 1-6 images of the same dog. The server averages the per-label scores | |
| // across every image — one photo alone can easily flip the top breed. | |
| estimateBreed: (files: File[], topN = 5) => { | |
| const fd = new FormData(); | |
| files.forEach((f) => fd.append("files", f)); | |
| fd.append("top_n", String(topN)); | |
| return request<BreedEstimateResult>("/search/breed", { method: "POST", body: fd }); | |
| }, | |
| // admin: datasets / data loading | |
| listAllDogs: ( | |
| kind: "all" | "known" | "unknown" = "all", | |
| limit = 30, | |
| offset = 0, | |
| opts: { | |
| breed?: string; | |
| breedK?: number; | |
| sort?: "newest" | "oldest"; | |
| zip?: string; | |
| addedFrom?: string; | |
| addedTo?: string; | |
| } = {} | |
| ) => { | |
| const p = new URLSearchParams({ kind, limit: String(limit), offset: String(offset) }); | |
| if (opts.breed) { | |
| p.set("breed", opts.breed); | |
| p.set("breed_k", String(opts.breedK ?? 10)); | |
| } | |
| if (opts.sort) p.set("sort", opts.sort); | |
| if (opts.zip) p.set("zip", opts.zip); | |
| if (opts.addedFrom) p.set("added_from", opts.addedFrom); | |
| if (opts.addedTo) p.set("added_to", opts.addedTo); | |
| return request<AdminDogsPage>(`/admin/dogs?${p}`); | |
| }, | |
| listBreeds: () => request<{ model: [string, string] | null; breeds: BreedOption[] }>("/admin/breeds"), | |
| listAllCases: (kind?: "lost" | "found", status?: string, limit = 30, offset = 0) => { | |
| const q = new URLSearchParams({ limit: String(limit), offset: String(offset) }); | |
| if (kind) q.set("kind", kind); | |
| if (status) q.set("status", status); | |
| return request<AdminCasesPage>(`/admin/cases?${q}`); | |
| }, | |
| listOwners: (limit = 30, offset = 0, q?: string) => { | |
| const p = new URLSearchParams({ limit: String(limit), offset: String(offset) }); | |
| if (q) p.set("q", q); | |
| return request<AdminOwnersPage>(`/admin/owners?${p}`); | |
| }, | |
| getOwnerDetail: (id: number) => request<AdminOwnerDetail>(`/admin/owners/${id}`), | |
| resetDemo: () => | |
| request<{ matches_reset: number; cases_reopened: number; known_relost: number; found_reset: number }>( | |
| "/admin/reset-demo", | |
| { method: "POST" }, | |
| ), | |
| getCaseDetail: (caseId: number) => request<AdminCaseDetail>(`/admin/cases/${caseId}`), | |
| runCaseMatch: (caseId: number) => | |
| request<Match[]>(`/admin/cases/${caseId}/run-match`, { method: "POST" }), | |
| deleteCase: (caseId: number) => | |
| request<{ deleted: Record<string, number> }>(`/admin/cases/${caseId}`, { method: "DELETE" }), | |
| deleteDog: (kind: "known" | "unknown", dogId: number) => | |
| request<{ deleted: Record<string, number> }>(`/admin/dogs/${kind}/${dogId}`, { method: "DELETE" }), | |
| setDogStatus: (kind: "known" | "unknown", dogId: number, status: string) => | |
| request<{ profile: AdminDog }>(`/admin/dogs/${kind}/${dogId}/status`, { | |
| method: "POST", | |
| body: JSON.stringify({ status }), | |
| }), | |
| deletePerson: (userId: number) => | |
| request<{ deleted: Record<string, number> }>(`/admin/owners/${userId}`, { method: "DELETE" }), | |
| testMatch: (kind: "known" | "unknown", dogId: number, topK = 10, applyBreedGate = false) => | |
| request<TestMatchResult>( | |
| `/admin/test-match?kind=${kind}&dog_id=${dogId}&top_k=${topK}&apply_breed_gate=${applyBreedGate}` | |
| ), | |
| getDogDetail: (kind: "known" | "unknown", dogId: number) => | |
| request<DogDetailResult>(`/admin/dog/${kind}/${dogId}`), | |
| listDatasets: () => request<Dataset[]>("/admin/datasets"), | |
| getDataset: (id: number) => request<DatasetDetail>(`/admin/datasets/${id}`), | |
| listDatasetDogs: (id: number, limit = 25, offset = 0) => | |
| request<DatasetDogsPage>(`/admin/datasets/${id}/dogs?limit=${limit}&offset=${offset}`), | |
| deleteDataset: (id: number) => | |
| request<PurgeResult>(`/admin/datasets/${id}`, { method: "DELETE" }), | |
| embedAll: (id: number) => | |
| request<EmbedAllResult>(`/admin/datasets/${id}/embed-all`, { method: "POST" }), | |
| startEmbedJob: (id: number) => | |
| request<{ job_id: string; status: string }>(`/admin/datasets/${id}/embed-all-job`, { | |
| method: "POST", | |
| }), | |
| matchDataset: (id: number, candidateDatasetId?: number) => | |
| request<MatchDatasetResult>( | |
| `/admin/datasets/${id}/match${candidateDatasetId ? `?candidate_dataset_id=${candidateDatasetId}` : ""}`, | |
| { method: "POST" } | |
| ), | |
| startLoad: (fd: FormData) => | |
| request<{ job_id: string; status: string }>("/admin/datasets/load", { method: "POST", body: fd }), | |
| getJob: (jobId: string) => request<Job>(`/admin/jobs/${jobId}`), | |
| }; | |