const API_BASE = typeof process.env.NEXT_PUBLIC_API_URL === "string" ? process.env.NEXT_PUBLIC_API_URL : ""; // Fallback to relative paths for single-origin deployments (HF/Nginx) async function request(path: string, options?: RequestInit): Promise { const url = path.startsWith("http") ? path : `${API_BASE}${path}`; const res = await fetch(url, { ...options, headers: { "Content-Type": "application/json", ...(options?.headers ?? {}) }, }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || "Request failed"); } if (res.status === 202) { throw new Error("202_ACCEPTED"); } return res.json(); } export interface SessionInfo { id: string; name: string; description: string | null; candidate_count: number; status: string; created_at: string; } export interface JD { id: string; title: string; raw_text: string; status: string; min_yoe: number | null; role_type: string | null; engineer_type: string | null; location: string | null; required_skills: string[]; jd_quality: JDQuality; custom_weights: Record; created_at: string; } export interface JDQuality { overall: "good" | "fair" | "poor"; vagueness_score: number; breadth_score: number; skill_count: number; contradictions: string[]; warnings: string[]; } export interface ComponentScores { semantic: number; skill: number; yoe: number; company: number; growth: number; education: number; } export interface GapItem { type: string; detail: string; mitigated_by_remote?: boolean; } export interface MatchedCandidate { candidate_id: string; rank: number; name: string | null; email: string | null; role_type: string | null; engineer_type: string | null; years_of_experience: number | null; most_recent_company: string | null; parsed_summary: string | null; programming_languages: string[]; growth_velocity: number; stage1_score: number; stage2_score: number | null; final_score: number; component_scores: ComponentScores; gaps: GapItem[]; } export interface MatchResponse { jd_id: string; jd_title: string; jd_quality: JDQuality; total_matched: number; results: MatchedCandidate[]; weights_used: Record; session_id: string | null; } export interface CandidateDetail { jd_id: string; candidate_id: string; rank: number | null; final_score: number; component_scores: ComponentScores; gaps: GapItem[]; explanation: string | null; candidate: Record; jd: Record; } export interface TaskStatus { task_id: string; status: string; result: unknown; } export const api = { createSession: (name: string, description?: string) => request("/api/sessions", { method: "POST", body: JSON.stringify({ name, description }) }), listSessions: () => request("/api/sessions"), getSession: (id: string) => request(`/api/sessions/${id}`), deleteSession: (id: string) => request(`/api/sessions/${id}`, { method: "DELETE" }), createJD: (title: string, raw_text: string, session_id?: string) => request("/api/jds", { method: "POST", body: JSON.stringify({ title, raw_text, session_id }) }), listJDs: (session_id?: string) => request(session_id ? `/api/jds?session_id=${session_id}` : "/api/jds"), getJD: (id: string) => request(`/api/jds/${id}`), updateJDWeights: (id: string, weights: Record) => request(`/api/jds/${id}/weights`, { method: "PATCH", body: JSON.stringify({ weights }) }), uploadCandidates: (file: File, sessionId?: string) => { const fd = new FormData(); fd.append("file", file); const url = sessionId ? `${API_BASE}/api/candidates/upload?session_id=${sessionId}` : `${API_BASE}/api/candidates/upload`; return fetch(url, { method: "POST", body: fd }).then((r) => { if (!r.ok) throw new Error("Upload failed"); return r.json(); }); }, candidateCount: (sessionId?: string) => { const url = sessionId ? `/api/candidates/count?session_id=${sessionId}` : "/api/candidates/count"; return request<{ count: number }>(url); }, taskStatus: (id: string) => request(`/api/candidates/status/${id}`), triggerMatch: async (jdId: string, sessionId?: string, stage1TopK: number = 100, stage2TopK: number = 40) => { const params = new URLSearchParams(); if (sessionId) params.set("session_id", sessionId); params.set("stage1_top_k", String(stage1TopK)); params.set("stage2_top_k", String(stage2TopK)); return request(`/api/match/${jdId}?${params.toString()}`, { method: "POST" }); }, getMatchResults: (jdId: string, sessionId?: string) => { const url = sessionId ? `/api/match/${jdId}?session_id=${sessionId}` : `/api/match/${jdId}`; return request(url); }, getCandidateDetail: (jdId: string, candidateId: string, sessionId?: string) => { const url = sessionId ? `/api/match/${jdId}/${candidateId}?session_id=${sessionId}` : `/api/match/${jdId}/${candidateId}`; return request(url); }, triggerExplanation: (jdId: string, candidateId: string, sessionId?: string) => { const url = sessionId ? `/api/match/${jdId}/candidates/${candidateId}/explain?session_id=${sessionId}` : `/api/match/${jdId}/candidates/${candidateId}/explain`; return request<{ status: string }>(url, { method: "POST" }); }, rerank: (jdId: string, weights: Record, sessionId?: string) => { const url = sessionId ? `/api/match/${jdId}/rerank?session_id=${sessionId}` : `/api/match/${jdId}/rerank`; return request(url, { method: "POST", body: JSON.stringify({ weights }) }); }, checkHealth: async () => { return request<{ status: string; version: string; qdrant: string; }>("/health"); } };