File size: 3,914 Bytes
722753e
 
 
 
900a32d
e4daa3b
1ce2fdc
 
 
 
 
900a32d
 
 
1ce2fdc
900a32d
1ce2fdc
 
 
 
900a32d
1ce2fdc
900a32d
1ce2fdc
900a32d
 
1ce2fdc
900a32d
1ce2fdc
 
 
e4daa3b
900a32d
e4daa3b
 
 
 
900a32d
 
 
e4daa3b
 
 
900a32d
e4daa3b
 
900a32d
e4daa3b
 
900a32d
 
e4daa3b
 
900a32d
 
 
 
e4daa3b
 
 
 
900a32d
e4daa3b
 
900a32d
e4daa3b
 
722753e
 
 
e4daa3b
900a32d
e4daa3b
 
900a32d
e4daa3b
 
 
900a32d
 
e4daa3b
 
900a32d
e4daa3b
 
722753e
 
 
 
 
 
 
e4daa3b
 
900a32d
722753e
e4daa3b
900a32d
e4daa3b
900a32d
e4daa3b
 
 
 
 
 
900a32d
e4daa3b
 
900a32d
e4daa3b
722753e
 
900a32d
 
722753e
 
900a32d
722753e
 
 
 
 
 
 
 
 
 
 
900a32d
722753e
 
 
900a32d
722753e
 
 
900a32d
722753e
900a32d
 
722753e
 
 
900a32d
722753e
 
e4daa3b
900a32d
 
e4daa3b
 
900a32d
e4daa3b
 
 
900a32d
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
import type {
  CasesResponse,
  CreateJobResponse,
  JobStatusResponse,
} from "../types";

/**
 * Safely parse JSON error response, logging failures in development.
 * Returns empty object if parsing fails (e.g., HTML error pages from proxies).
 * (BUG-013 fix: was silently returning {} without any logging)
 */
async function parseErrorJson(
  response: Response,
): Promise<{ detail?: string }> {
  try {
    return await response.json();
  } catch (parseError) {
    // Log in development to help debug malformed responses
    if (import.meta.env.DEV) {
      console.warn(
        "Failed to parse error response as JSON:",
        parseError,
        "Status:",
        response.status,
        response.statusText,
      );
    }
    return {};
  }
}

function getApiBase(): string {
  const url = import.meta.env.VITE_API_URL;

  // In production, VITE_API_URL must be set - fail fast with clear error
  if (import.meta.env.PROD && !url) {
    throw new Error(
      "VITE_API_URL environment variable is required in production. " +
        "Set it to the backend API URL (e.g., https://your-app.hf.space).",
    );
  }

  // In development, fall back to localhost
  return url || "http://localhost:7860";
}

const API_BASE = getApiBase();

export class ApiError extends Error {
  status: number;
  detail?: string;

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

class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  /**
   * Get list of available cases
   */
  async getCases(signal?: AbortSignal): Promise<CasesResponse> {
    const response = await fetch(`${this.baseUrl}/api/cases`, { signal });

    if (!response.ok) {
      const error = await parseErrorJson(response);
      throw new ApiError(
        `Failed to fetch cases: ${response.statusText}`,
        response.status,
        error.detail,
      );
    }

    return response.json();
  }

  /**
   * Create a segmentation job (async - returns immediately with job ID)
   *
   * The actual ML inference runs in the background. Poll getJobStatus()
   * to track progress and retrieve results when complete.
   */
  async createSegmentJob(
    caseId: string,
    fastMode: boolean = true,
    signal?: AbortSignal,
  ): Promise<CreateJobResponse> {
    const response = await fetch(`${this.baseUrl}/api/segment`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        case_id: caseId,
        fast_mode: fastMode,
      }),
      signal,
    });

    if (!response.ok) {
      const error = await parseErrorJson(response);
      throw new ApiError(
        `Failed to create job: ${error.detail || response.statusText}`,
        response.status,
        error.detail,
      );
    }

    return response.json();
  }

  /**
   * Get the status of a segmentation job
   *
   * Poll this endpoint to track progress and retrieve results.
   * When status is 'completed', the result field contains segmentation data.
   * When status is 'failed', the error field contains the error message.
   */
  async getJobStatus(
    jobId: string,
    signal?: AbortSignal,
  ): Promise<JobStatusResponse> {
    const response = await fetch(`${this.baseUrl}/api/jobs/${jobId}`, {
      signal,
    });

    if (response.status === 404) {
      throw new ApiError(
        "Job not found or expired",
        404,
        "Jobs expire after 1 hour",
      );
    }

    if (!response.ok) {
      const error = await parseErrorJson(response);
      throw new ApiError(
        `Failed to get job status: ${error.detail || response.statusText}`,
        response.status,
        error.detail,
      );
    }

    return response.json();
  }
}

export const apiClient = new ApiClient(API_BASE);