File size: 3,325 Bytes
b3d14e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Tiny typed fetch client for the FastAPI backend.
 *
 * Each function maps 1:1 to a backend route in
 * `webapp/backend/routes/`. The base URL is empty by default so the
 * calls are relative — Vite's dev proxy forwards them to
 * http://localhost:8000 (see `vite.config.ts`), and a built bundle
 * served from the same FastAPI server gets them for free. Override
 * via `VITE_API_BASE` for split-host deployments.
 */

import type {
  EvaluateRequest,
  EvaluateResponse,
  HealthResponse,
  OptimizeCancelResponse,
  OptimizeJobResponse,
  OptimizeRequest,
  OptimizeResultResponse,
  PredictRequest,
  PredictResponse,
  RegistryListResponse,
  ScenarioListResponse,
  ShapExplainRequest,
  ShapLocalResponse,
  SweepRequest,
  SweepResponse,
  VersionResponse,
} from "@/types/api";

const API_BASE = (import.meta.env.VITE_API_BASE ?? "") as string;

export function apiUrl(path: string): string {
  return `${API_BASE}${path}`;
}

class ApiError extends Error {
  status: number;
  body: unknown;

  constructor(status: number, message: string, body: unknown) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
  }
}

async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
  const url = apiUrl(path);
  const headers = new Headers(init.headers);
  if (init.body && !headers.has("content-type")) {
    headers.set("content-type", "application/json");
  }
  const response = await fetch(url, { ...init, headers });
  const text = await response.text();
  const body: unknown = text ? safeJson(text) : null;
  if (!response.ok) {
    const detail = (body as { detail?: string } | null)?.detail;
    throw new ApiError(
      response.status,
      detail ?? `HTTP ${response.status} on ${path}`,
      body,
    );
  }
  return body as T;
}

function safeJson(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

export const api = {
  healthz: () => request<HealthResponse>("/healthz"),
  version: () => request<VersionResponse>("/version"),
  listScenarios: () => request<ScenarioListResponse>("/scenarios"),
  listRegistry: () => request<RegistryListResponse>("/registry"),
  predict: (req: PredictRequest) =>
    request<PredictResponse>("/predict", {
      method: "POST",
      body: JSON.stringify(req),
    }),
  evaluate: (req: EvaluateRequest) =>
    request<EvaluateResponse>("/evaluate", {
      method: "POST",
      body: JSON.stringify(req),
    }),
  sweep: (req: SweepRequest) =>
    request<SweepResponse>("/sweep", {
      method: "POST",
      body: JSON.stringify(req),
    }),
  optimize: (req: OptimizeRequest) =>
    request<OptimizeJobResponse>("/optimize", {
      method: "POST",
      body: JSON.stringify(req),
    }),
  optimizeResult: (pathOrJobId: string) =>
    request<OptimizeResultResponse>(
      pathOrJobId.startsWith("/") ? pathOrJobId : `/optimize/${pathOrJobId}/result`,
    ),
  cancelOptimize: (pathOrJobId: string) =>
    request<OptimizeCancelResponse>(
      pathOrJobId.startsWith("/") ? pathOrJobId : `/optimize/${pathOrJobId}/cancel`,
      { method: "POST" },
    ),
  shapExplain: (req: ShapExplainRequest) =>
    request<ShapLocalResponse>("/shap/explain", {
      method: "POST",
      body: JSON.stringify(req),
    }),
};

export { ApiError };