File size: 3,239 Bytes
497bb49
 
 
 
 
fbf73ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497bb49
 
 
fbf73ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497bb49
 
 
 
 
 
 
fbf73ff
 
 
497bb49
fbf73ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497bb49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fbf73ff
 
 
 
 
 
 
 
 
 
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
import type {
  CasesResponse,
  CreateJobResponse,
  JobStatusResponse,
} from '../types'

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 response.json().catch(() => ({}))
      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 response.json().catch(() => ({}))
      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 response.json().catch(() => ({}))
      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)