import type { AuthMeResponse, AuthRequest, AuthResponse, DiagnosisRecord, DiseaseCatalogItem, DiseaseSlug, ImageClassificationRecord, ManualClassificationRequest, ManualClassificationRecord, RegisterRequest, ReviewDiagnosisRequest, DashboardSummary, } from '@zeavis/shared'; import { recordApiCall } from './telemetry'; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''; export interface ApiError extends Error { status: number; source?: 'uploader' | 'model-service' | 'unknown'; } async function fetchApi(endpoint: string, options?: RequestInit): Promise { const start = performance.now(); const url = `${apiBaseUrl}${endpoint}`; const response = await fetch(url, { credentials: 'include', ...options, headers: options?.headers, }); const duration = performance.now() - start; const method = options?.method ?? 'GET'; recordApiCall(method, endpoint, duration, response.status); if (!response.ok) { let errorMessage = `HTTP ${response.status}`; let source: 'uploader' | 'model-service' | 'unknown' | undefined; try { const errorData = await response.json(); if (errorData.error) { errorMessage = errorData.error; } if (errorData.source) { source = errorData.source; } } catch { } const error = new Error(errorMessage) as ApiError; error.status = response.status; error.source = source; throw error; } return response.json(); } function toImageClassificationRecord(diagnosis: DiagnosisRecord): ImageClassificationRecord { if (!diagnosis.disease) { throw new Error('Diagnosis does not include disease details'); } return { id: diagnosis.id, predictedDiseaseSlug: diagnosis.predictedDiseaseSlug || 'daun-sehat', confidence: diagnosis.confidence || 0, probabilities: diagnosis.predictions.map((p) => ({ diseaseSlug: p.diseaseSlug || 'daun-sehat', label: p.modelLabel, confidence: p.confidence, })), imageUrl: diagnosis.imageUrl, originalFileName: diagnosis.imageFileName, uploaderPublicId: diagnosis.uploaderPublicId, uploader: { public_id: diagnosis.uploaderPublicId, file_name: diagnosis.imageFileName, mime_type: diagnosis.imageMimeType, size_bytes: diagnosis.imageSizeBytes, file_type: diagnosis.imageMimeType.split('/')[1] || 'unknown', download_url: diagnosis.imageUrl, }, createdAt: diagnosis.createdAt, disease: diagnosis.disease, }; } export const apiClient = { // Auth methods async getMe(): Promise { return fetchApi('/api/v1/auth/me'); }, async register(payload: RegisterRequest): Promise { return fetchApi('/api/v1/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); }, async login(payload: AuthRequest): Promise { return fetchApi('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); }, async logout(): Promise<{ ok: boolean }> { return fetchApi('/api/v1/auth/logout', { method: 'POST' }); }, // Disease catalog methods async getDiseases(): Promise { return fetchApi('/api/v1/diseases'); }, async getDisease(slug: DiseaseSlug): Promise { return fetchApi(`/api/v1/diseases/${slug}`); }, // Manual classification methods async getManualClassifications(): Promise { return fetchApi('/api/v1/classifications/manual'); }, async createManualClassification( payload: ManualClassificationRequest, ): Promise { return fetchApi('/api/v1/classifications/manual', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); }, // Diagnosis methods async getDiagnoses(): Promise { return fetchApi('/api/v1/diagnoses'); }, async getDiagnosis(id: string): Promise { return fetchApi(`/api/v1/diagnoses/${id}`); }, async createDiagnosis(file: File): Promise { const formData = new FormData(); formData.append('file', file); return fetchApi('/api/v1/diagnoses', { method: 'POST', body: formData, }); }, // Expert review methods async getExpertReviews(): Promise { return fetchApi('/api/v1/expert/reviews'); }, async reviewDiagnosis(id: string, payload: ReviewDiagnosisRequest): Promise { return fetchApi(`/api/v1/expert/reviews/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); }, // Dashboard methods async getDashboardSummary(): Promise { return fetchApi('/api/v1/dashboard/summary'); }, // Backwards-compatible aliases for old image classification methods async getImageClassifications(): Promise { const diagnoses = await this.getDiagnoses(); return diagnoses.map(toImageClassificationRecord); }, async createImageClassification(file: File): Promise { const diagnosis = await this.createDiagnosis(file); return toImageClassificationRecord(diagnosis); }, };