| const API_BASE = typeof process.env.NEXT_PUBLIC_API_URL === "string" |
| ? process.env.NEXT_PUBLIC_API_URL |
| : ""; |
|
|
| async function request<T>(path: string, options?: RequestInit): Promise<T> { |
| 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<string, number>; |
| 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<string, number>; |
| 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<string, unknown>; |
| jd: Record<string, unknown>; |
| } |
|
|
| export interface TaskStatus { |
| task_id: string; |
| status: string; |
| result: unknown; |
| } |
|
|
| export const api = { |
| createSession: (name: string, description?: string) => |
| request<SessionInfo>("/api/sessions", { method: "POST", body: JSON.stringify({ name, description }) }), |
| listSessions: () => request<SessionInfo[]>("/api/sessions"), |
| getSession: (id: string) => request<SessionInfo>(`/api/sessions/${id}`), |
| deleteSession: (id: string) => request<void>(`/api/sessions/${id}`, { method: "DELETE" }), |
|
|
| createJD: (title: string, raw_text: string, session_id?: string) => |
| request<JD>("/api/jds", { method: "POST", body: JSON.stringify({ title, raw_text, session_id }) }), |
| listJDs: (session_id?: string) => request<JD[]>(session_id ? `/api/jds?session_id=${session_id}` : "/api/jds"), |
| getJD: (id: string) => request<JD>(`/api/jds/${id}`), |
| updateJDWeights: (id: string, weights: Record<string, number>) => |
| request<JD>(`/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<TaskStatus>(`/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<MatchResponse>(`/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<MatchResponse>(url); |
| }, |
|
|
| getCandidateDetail: (jdId: string, candidateId: string, sessionId?: string) => { |
| const url = sessionId |
| ? `/api/match/${jdId}/${candidateId}?session_id=${sessionId}` |
| : `/api/match/${jdId}/${candidateId}`; |
| return request<CandidateDetail>(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<string, number>, sessionId?: string) => { |
| const url = sessionId |
| ? `/api/match/${jdId}/rerank?session_id=${sessionId}` |
| : `/api/match/${jdId}/rerank`; |
| return request<MatchResponse>(url, { method: "POST", body: JSON.stringify({ weights }) }); |
| }, |
|
|
| checkHealth: async () => { |
| return request<{ status: string; version: string; qdrant: string; }>("/health"); |
| } |
| }; |
|
|