cloud450 commited on
Commit
ab13a8a
·
verified ·
1 Parent(s): e33e49c

Upload 42 files

Browse files
Files changed (42) hide show
  1. proj/.vscode/launch.json +33 -0
  2. proj/.vscode/tasks.json +26 -0
  3. proj/README.md +76 -0
  4. proj/backend/.env.example +3 -0
  5. proj/backend/app/__pycache__/main.cpython-312.pyc +0 -0
  6. proj/backend/app/main.py +85 -0
  7. proj/backend/app/models/__pycache__/schemas.cpython-312.pyc +0 -0
  8. proj/backend/app/models/schemas.py +64 -0
  9. proj/backend/app/prompts/__pycache__/templates.cpython-312.pyc +0 -0
  10. proj/backend/app/prompts/templates.py +116 -0
  11. proj/backend/app/services/__pycache__/evaluation_service.cpython-312.pyc +0 -0
  12. proj/backend/app/services/__pycache__/matching_service.cpython-312.pyc +0 -0
  13. proj/backend/app/services/evaluation_service.py +124 -0
  14. proj/backend/app/services/matching_service.py +58 -0
  15. proj/backend/app/utils/__pycache__/groq_client.cpython-312.pyc +0 -0
  16. proj/backend/app/utils/__pycache__/key_manager.cpython-312.pyc +0 -0
  17. proj/backend/app/utils/groq_client.py +32 -0
  18. proj/backend/app/utils/key_manager.py +27 -0
  19. proj/backend/requirements.txt +12 -0
  20. proj/frontend/.env.local.example +1 -0
  21. proj/frontend/.gitignore +41 -0
  22. proj/frontend/AGENTS.md +5 -0
  23. proj/frontend/CLAUDE.md +1 -0
  24. proj/frontend/eslint.config.mjs +16 -0
  25. proj/frontend/jsconfig.json +7 -0
  26. proj/frontend/next.config.mjs +6 -0
  27. proj/frontend/package-lock.json +0 -0
  28. proj/frontend/package.json +27 -0
  29. proj/frontend/postcss.config.mjs +7 -0
  30. proj/frontend/public/file.svg +1 -0
  31. proj/frontend/public/globe.svg +1 -0
  32. proj/frontend/public/next.svg +1 -0
  33. proj/frontend/public/vercel.svg +1 -0
  34. proj/frontend/public/window.svg +1 -0
  35. proj/frontend/src/app/favicon.ico +0 -0
  36. proj/frontend/src/app/globals.css +62 -0
  37. proj/frontend/src/app/layout.jsx +36 -0
  38. proj/frontend/src/app/page.jsx +171 -0
  39. proj/frontend/src/components/CandidateDetail.jsx +154 -0
  40. proj/frontend/src/components/CandidateTable.jsx +81 -0
  41. proj/frontend/src/lib/api.js +26 -0
  42. proj/frontend/src/lib/api.ts +26 -0
proj/.vscode/launch.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Debug Backend (FastAPI)",
6
+ "type": "python",
7
+ "request": "launch",
8
+ "module": "uvicorn",
9
+ "cwd": "${workspaceFolder}/backend",
10
+ "args": [
11
+ "app.main:app",
12
+ "--reload",
13
+ "--port",
14
+ "8000"
15
+ ],
16
+ "jinja": true,
17
+ "justMyCode": true
18
+ },
19
+ {
20
+ "name": "Debug Frontend (Next.js)",
21
+ "type": "chrome",
22
+ "request": "launch",
23
+ "url": "http://localhost:3000",
24
+ "webRoot": "${workspaceFolder}/frontend"
25
+ }
26
+ ],
27
+ "compounds": [
28
+ {
29
+ "name": "Debug All",
30
+ "configurations": ["Debug Backend (FastAPI)", "Debug Frontend (Next.js)"]
31
+ }
32
+ ]
33
+ }
proj/.vscode/tasks.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "Backend",
6
+ "type": "shell",
7
+ "command": "cd backend && .venv/Scripts/python -m uvicorn app.main:app --reload",
8
+ "presentation": {
9
+ "panel": "dedicated"
10
+ }
11
+ },
12
+ {
13
+ "label": "Frontend",
14
+ "type": "shell",
15
+ "command": "cd frontend && npm run dev",
16
+ "presentation": {
17
+ "panel": "dedicated"
18
+ }
19
+ },
20
+
21
+ {
22
+ "label": "Start All",
23
+ "dependsOn": ["Backend", "Frontend"]
24
+ }
25
+ ]
26
+ }
proj/README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Full-Stack AI Recruitment Engine
2
+
3
+ A production-ready candidate ranking platform built with Next.js, FastAPI, and Groq LLMs.
4
+
5
+ ## Features
6
+ - **Multi-Agent Evaluation**: Sequential and parallel agent pipeline (Signal Extraction, Founder Eval, Tech Eval, HR Agent, etc.).
7
+ - **Groq Key Rotation**: Automatic round-robin cycling of multiple API keys to prevent rate limits.
8
+ - **CSV Data Ingestion**: Parse resumes and candidate data directly from CSV files.
9
+ - **Glassmorphic UI**: Modern, high-performance dashboard with animations and progress tracking.
10
+
11
+ ---
12
+
13
+ ## Backend Setup (FastAPI)
14
+
15
+ 1. **Navigate to backend**:
16
+ ```bash
17
+ cd backend
18
+ ```
19
+
20
+ 2. **Create and Activate Virtual Environment**:
21
+ ```bash
22
+ python -m venv venv
23
+ # Windows:
24
+ venv\Scripts\activate
25
+ ```
26
+
27
+ 3. **Install Dependencies**:
28
+ ```bash
29
+ pip install -r requirements.txt
30
+ ```
31
+
32
+ 4. **Environment Variables**:
33
+ Create a `.env` file in the `backend/` folder based on `.env.example`:
34
+ ```env
35
+ GROQ_API_KEYS=key1,key2,key3
36
+ GROQ_MODEL=llama3-70b-8192
37
+ PORT=8000
38
+ ```
39
+
40
+ 5. **Run Backend**:
41
+ ```bash
42
+ uvicorn app.main:app --reload
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Frontend Setup (Next.js)
48
+
49
+ 1. **Navigate to frontend**:
50
+ ```bash
51
+ cd frontend
52
+ ```
53
+
54
+ 2. **Install Dependencies**:
55
+ ```bash
56
+ npm install
57
+ ```
58
+
59
+ 3. **Environment Variables**:
60
+ Create a `.env.local` file in the `frontend/` folder:
61
+ ```env
62
+ NEXT_PUBLIC_API_URL=http://localhost:8000
63
+ ```
64
+
65
+ 4. **Run Frontend**:
66
+ ```bash
67
+ npm run dev
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Usage Guide
73
+ 1. **Paste Job Description**: Enter the target role details in the textarea.
74
+ 2. **Upload CSV**: Upload a CSV containing candidate data (columns: `name`, `email`, `skills`, `experience`, `projects`, `education`, `resume_text`).
75
+ 3. **Evaluate**: Click "Run Evaluation" and watch the AI agents process each candidate.
76
+ 4. **Deep Dive**: Click on any candidate row to see the full multi-agent breakdown and final synthesis.
proj/backend/.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ GROQ_API_KEYS=gsk_your_key_1,gsk_your_key_2,gsk_your_key_3
2
+ GROQ_MODEL=llama3-70b-8192
3
+ PORT=8000
proj/backend/app/__pycache__/main.cpython-312.pyc ADDED
Binary file (4.54 kB). View file
 
proj/backend/app/main.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ load_dotenv()
4
+
5
+ import pandas as pd
6
+ import io
7
+ import uuid
8
+ from fastapi import FastAPI, UploadFile, File, HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from app.models.schemas import EvaluationRequest, EvaluationResponse, Candidate
11
+ from app.services.evaluation_service import evaluate_candidate
12
+ import asyncio
13
+
14
+ app = FastAPI(title="AI Recruitment Engine")
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ # In-memory storage for demonstration (Production should use DB)
24
+ evaluations_cache = {}
25
+
26
+ @app.post("/upload-csv")
27
+ async def upload_csv(file: UploadFile = File(...)):
28
+ if not file.filename.endswith('.csv'):
29
+ raise HTTPException(status_code=400, detail="Invalid file format. Please upload a CSV.")
30
+
31
+ try:
32
+ content = await file.read()
33
+ df = pd.read_csv(io.BytesIO(content))
34
+ df = df.fillna("")
35
+
36
+ candidates = []
37
+ for _, row in df.iterrows():
38
+ candidates.append(Candidate(
39
+ id=str(uuid.uuid4()),
40
+ name=str(row.get("name", "Unknown")),
41
+ email=str(row.get("email", "")),
42
+ skills=str(row.get("skills", "")),
43
+ experience=str(row.get("experience", "")),
44
+ projects=str(row.get("projects", "")),
45
+ education=str(row.get("education", "")),
46
+ resume_text=str(row.get("resume_text", ""))
47
+ ))
48
+
49
+ return {"candidates": candidates}
50
+ except Exception as e:
51
+ raise HTTPException(status_code=500, detail=f"Error parsing CSV: {str(e)}")
52
+
53
+ @app.post("/evaluate", response_model=EvaluationResponse)
54
+ async def evaluate(request: EvaluationRequest):
55
+ if not request.jd:
56
+ raise HTTPException(status_code=400, detail="Job Description is required.")
57
+
58
+ if not request.candidates:
59
+ raise HTTPException(status_code=400, detail="No candidates provided.")
60
+
61
+ from app.services.evaluation_service import perform_hybrid_evaluation
62
+ response = await perform_hybrid_evaluation(request.jd, request.candidates)
63
+
64
+ # Store in cache for detail retrieval
65
+ for rank in response.shortlist:
66
+ evaluations_cache[rank.candidate_id] = rank
67
+
68
+ # Also store the deep review details
69
+ evaluations_cache.update(response.details)
70
+
71
+ return response
72
+
73
+ @app.get("/results")
74
+ async def get_results():
75
+ return list(evaluations_cache.values())
76
+
77
+ @app.get("/candidate/{id}")
78
+ async def get_candidate_report(id: str):
79
+ if id not in evaluations_cache:
80
+ raise HTTPException(status_code=404, detail="Candidate report not found.")
81
+ return evaluations_cache[id]
82
+
83
+ if __name__ == "__main__":
84
+ import uvicorn
85
+ uvicorn.run(app, host="0.0.0.0", port=8000)
proj/backend/app/models/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (3.26 kB). View file
 
proj/backend/app/models/schemas.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional, Dict, Any
3
+
4
+ class Candidate(BaseModel):
5
+ id: str
6
+ name: str
7
+ email: Optional[str] = None
8
+ skills: Optional[str] = None
9
+ experience: Optional[str] = None
10
+ projects: Optional[str] = None
11
+ education: Optional[str] = None
12
+ resume_text: Optional[str] = None
13
+ # Add other fields from the CSV if needed
14
+ data: Optional[Dict[str, Any]] = None
15
+
16
+ class NormalizedCandidate(BaseModel):
17
+ candidate_id: str
18
+ name: str
19
+ normalized_title: str
20
+ experience_years: float
21
+ primary_skills: List[str]
22
+ secondary_skills: List[str]
23
+ backend_score: float
24
+ frontend_score: float
25
+ cloud_score: float
26
+ database_score: float
27
+ notice_period_days: int
28
+ location: str
29
+ employment_status: str
30
+ salary_expectation: str
31
+ flags: List[str]
32
+
33
+ class RerankResult(BaseModel):
34
+ candidate_id: str
35
+ scores: Dict[str, float]
36
+ final_score: float
37
+ decision: str
38
+
39
+ class DeepReview(BaseModel):
40
+ candidate_id: str
41
+ verdict: str
42
+ why: str
43
+ strengths: List[str]
44
+ risks: List[str]
45
+ hidden_signal: str
46
+ confidence: float
47
+
48
+ class FinalRank(BaseModel):
49
+ rank: int
50
+ candidate_id: str
51
+ name: str
52
+ decision: str
53
+ reason: str
54
+
55
+ class FinalShortlist(BaseModel):
56
+ final_ranking: List[FinalRank]
57
+
58
+ class EvaluationRequest(BaseModel):
59
+ jd: str
60
+ candidates: List[Candidate]
61
+
62
+ class EvaluationResponse(BaseModel):
63
+ shortlist: List[FinalRank]
64
+ details: Dict[str, Any] # Map candidate_id to their full review for UI
proj/backend/app/prompts/__pycache__/templates.cpython-312.pyc ADDED
Binary file (2.17 kB). View file
 
proj/backend/app/prompts/templates.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # STAGE 1 — STRUCTURED PIPELINE EXTRACTION PROMPT
2
+ STAGE1_NORMALIZATION_PROMPT = """
3
+ ROLE: You are a candidate normalization system. You receive already-structured candidate CSV data.
4
+ Your task is to clean, normalize, and prepare candidate features for ranking.
5
+
6
+ JOB DESCRIPTION:
7
+ {jd}
8
+
9
+ CANDIDATE DATA:
10
+ {candidate_raw}
11
+
12
+ TASK: Return normalized structured JSON only.
13
+ RULES: No opinions, No ranking, No explanations, No hallucination, Use only available fields, Standardize values.
14
+
15
+ OUTPUT JSON FORMAT:
16
+ {{
17
+ "candidate_id": "",
18
+ "name": "",
19
+ "normalized_title": "",
20
+ "experience_years": 0,
21
+ "primary_skills": [],
22
+ "secondary_skills": [],
23
+ "backend_score": 0,
24
+ "frontend_score": 0,
25
+ "cloud_score": 0,
26
+ "database_score": 0,
27
+ "notice_period_days": 0,
28
+ "location": "",
29
+ "employment_status": "",
30
+ "salary_expectation": "",
31
+ "flags": []
32
+ }}
33
+ """
34
+
35
+ # STAGE 3 — DETERMINISTIC RERANKING PROMPT
36
+ STAGE3_RERANK_PROMPT = """
37
+ ROLE: You are a candidate scoring engine. Use structured candidate features and JD requirements to assign weighted scores.
38
+
39
+ JOB DESCRIPTION:
40
+ {jd}
41
+
42
+ CANDIDATE_FEATURES:
43
+ {normalized_candidate}
44
+
45
+ TASK: Score candidate using deterministic logic.
46
+ WEIGHTS:
47
+ - Skill Match = 35%
48
+ - Experience Match = 25%
49
+ - Role Relevance = 20%
50
+ - Cloud/Infra Fit = 10%
51
+ - Notice Period = 10%
52
+
53
+ OUTPUT JSON:
54
+ {{
55
+ "candidate_id": "",
56
+ "scores": {{
57
+ "skill_match": 0,
58
+ "experience_match": 0,
59
+ "role_relevance": 0,
60
+ "infra_fit": 0,
61
+ "notice_fit": 0
62
+ }},
63
+ "final_score": 0,
64
+ "decision": "pass / reject"
65
+ }}
66
+ """
67
+
68
+ # STAGE 4 — LLM DEEP REVIEW PROMPT
69
+ STAGE4_DEEP_REVIEW_PROMPT = """
70
+ ROLE: You are a senior hiring evaluator. You receive only top shortlisted candidates.
71
+ Use nuanced reasoning to identify strongest hires.
72
+
73
+ JOB DESCRIPTION:
74
+ {jd}
75
+
76
+ CANDIDATE:
77
+ {candidate_data}
78
+
79
+ RERANK_SCORE:
80
+ {score}
81
+
82
+ TASK: Evaluate hidden strengths, practical fit, risks, and hiring recommendation.
83
+ RULES: Use candidate evidence only, No hallucinations, Be concise, Be decisive.
84
+
85
+ OUTPUT JSON:
86
+ {{
87
+ "verdict": "strong hire / hire / consider / reject",
88
+ "why": "",
89
+ "strengths": [],
90
+ "risks": [],
91
+ "hidden_signal": "",
92
+ "confidence": 0
93
+ }}
94
+ """
95
+
96
+ # STAGE 5 — FINAL SELECTION PROMPT
97
+ STAGE5_FINAL_SELECTION_PROMPT = """
98
+ ROLE: You are the final hiring decision engine. Combine rerank scores and LLM evaluations into final shortlist.
99
+
100
+ TOP_CANDIDATES:
101
+ {all_top_5_results}
102
+
103
+ TASK: Return final ordered ranking.
104
+ OUTPUT JSON:
105
+ {{
106
+ "final_ranking": [
107
+ {{
108
+ "rank": 1,
109
+ "candidate_id": "",
110
+ "name": "",
111
+ "decision": "",
112
+ "reason": ""
113
+ }}
114
+ ]
115
+ }}
116
+ """
proj/backend/app/services/__pycache__/evaluation_service.cpython-312.pyc ADDED
Binary file (8.46 kB). View file
 
proj/backend/app/services/__pycache__/matching_service.cpython-312.pyc ADDED
Binary file (3.15 kB). View file
 
proj/backend/app/services/evaluation_service.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import re
4
+ from typing import List, Dict, Any
5
+ from app.utils.groq_client import get_groq_completion
6
+ from app.models.schemas import (
7
+ Candidate, NormalizedCandidate, RerankResult,
8
+ DeepReview, FinalShortlist, FinalRank, EvaluationResponse
9
+ )
10
+ from app.services.matching_service import match_service
11
+ from app.prompts.templates import (
12
+ STAGE1_NORMALIZATION_PROMPT,
13
+ STAGE3_RERANK_PROMPT,
14
+ STAGE4_DEEP_REVIEW_PROMPT,
15
+ STAGE5_FINAL_SELECTION_PROMPT
16
+ )
17
+
18
+ # Concurrency Throttling
19
+ sem = asyncio.Semaphore(3)
20
+
21
+ async def get_completion_with_sem(messages):
22
+ async with sem:
23
+ return await get_groq_completion(messages)
24
+
25
+ async def normalize_candidate(jd: str, candidate: Candidate) -> NormalizedCandidate:
26
+ candidate_raw = candidate.model_dump_json()
27
+ resp = await get_completion_with_sem([
28
+ {"role": "system", "content": "You are a professional data normalizer. Output JSON ONLY."},
29
+ {"role": "user", "content": STAGE1_NORMALIZATION_PROMPT.format(jd=jd, candidate_raw=candidate_raw)}
30
+ ])
31
+ try:
32
+ match = re.search(r'\{.*\}', resp, re.DOTALL)
33
+ data = json.loads(match.group() if match else resp)
34
+ return NormalizedCandidate(**data)
35
+ except Exception as e:
36
+ print(f"Failed to normalize {candidate.name}: {e}")
37
+ # Return a fallback object
38
+ return NormalizedCandidate(
39
+ candidate_id=candidate.id, name=candidate.name, normalized_title="Unknown",
40
+ experience_years=0, primary_skills=[], secondary_skills=[],
41
+ backend_score=0, frontend_score=0, cloud_score=0, database_score=0,
42
+ notice_period_days=0, location="Unknown", employment_status="Unknown",
43
+ salary_expectation="Unknown", flags=["Parsing Error"]
44
+ )
45
+
46
+ async def rerank_candidate(jd: str, normalized: NormalizedCandidate) -> RerankResult:
47
+ resp = await get_completion_with_sem([
48
+ {"role": "system", "content": "You are a recruitment scoring engine. Output JSON ONLY."},
49
+ {"role": "user", "content": STAGE3_RERANK_PROMPT.format(jd=jd, normalized_candidate=normalized.model_dump_json())}
50
+ ])
51
+ try:
52
+ match = re.search(r'\{.*\}', resp, re.DOTALL)
53
+ data = json.loads(match.group() if match else resp)
54
+ return RerankResult(**data)
55
+ except:
56
+ return RerankResult(candidate_id=normalized.candidate_id, scores={}, final_score=0, decision="reject")
57
+
58
+ async def review_candidate(jd: str, candidate_data: str, score: float, cand_id: str) -> DeepReview:
59
+ resp = await get_completion_with_sem([
60
+ {"role": "system", "content": "You are a senior hiring evaluator. Output JSON ONLY."},
61
+ {"role": "user", "content": STAGE4_DEEP_REVIEW_PROMPT.format(jd=jd, candidate_data=candidate_data, score=score)}
62
+ ])
63
+ try:
64
+ match = re.search(r'\{.*\}', resp, re.DOTALL)
65
+ data = json.loads(match.group() if match else resp)
66
+ data["candidate_id"] = cand_id
67
+ return DeepReview(**data)
68
+ except:
69
+ return DeepReview(candidate_id=cand_id, verdict="reject", why="Error in evaluation", strengths=[], risks=[], hidden_signal="", confidence=0)
70
+
71
+ async def perform_hybrid_evaluation(jd: str, candidates: List[Candidate]) -> EvaluationResponse:
72
+ # 1. Normalization (Stage 1) - All candidates
73
+ normalization_tasks = [normalize_candidate(jd, c) for c in candidates]
74
+ normalized_candidates = await asyncio.gather(*normalization_tasks)
75
+
76
+ # Map for easy lookup
77
+ normalized_map = {n.candidate_id: n for n in normalized_candidates}
78
+ candidate_map = {c.id: c for c in candidates}
79
+
80
+ # 2. Embedding Matching (Stage 2) - Retrieves Top 20
81
+ # We pass the normalized summary/skills for better matching
82
+ top_20 = await match_service.get_top_candidates(jd, candidates)
83
+
84
+ # 3. Deterministic Reranking (Stage 3) - Top 20 -> Top 10
85
+ rerank_tasks = [rerank_candidate(jd, normalized_map[c.id]) for c in top_20]
86
+ rerank_results = await asyncio.gather(*rerank_tasks)
87
+ rerank_results.sort(key=lambda x: x.final_score, reverse=True)
88
+ top_10_results = rerank_results[:10]
89
+
90
+ # 4. LLM Deep Review (Stage 4) - Top 5 Only
91
+ top_5_for_review = top_10_results[:5]
92
+ review_tasks = [
93
+ review_candidate(
94
+ jd,
95
+ candidate_map[r.candidate_id].model_dump_json(),
96
+ r.final_score,
97
+ r.candidate_id
98
+ ) for r in top_5_for_review
99
+ ]
100
+ review_results = await asyncio.gather(*review_tasks)
101
+ review_map = {rev.candidate_id: rev for rev in review_results}
102
+
103
+ # 5. Final Selection (Stage 5)
104
+ reviews_json = json.dumps([r.model_dump() for r in review_results])
105
+ final_resp = await get_completion_with_sem([
106
+ {"role": "system", "content": "You are the final hiring decision officer. Output JSON ONLY."},
107
+ {"role": "user", "content": STAGE5_FINAL_SELECTION_PROMPT.format(all_top_5_results=reviews_json)}
108
+ ])
109
+
110
+ try:
111
+ match = re.search(r'\{.*\}', final_resp, re.DOTALL)
112
+ final_data = json.loads(match.group() if match else final_resp)
113
+ shortlist = FinalShortlist(**final_data)
114
+ except:
115
+ # Fallback ranking if synthesis fails
116
+ shortlist = FinalShortlist(final_ranking=[
117
+ FinalRank(rank=i+1, candidate_id=r.candidate_id, name=candidate_map[r.candidate_id].name, decision=r.decision, reason="Automatic ranking")
118
+ for i, r in enumerate(top_5_for_review)
119
+ ])
120
+
121
+ return EvaluationResponse(
122
+ shortlist=shortlist.final_ranking,
123
+ details={rev.candidate_id: rev.model_dump() for rev in review_results}
124
+ )
proj/backend/app/services/matching_service.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pinecone import Pinecone
3
+ from sentence_transformers import SentenceTransformer
4
+ from typing import List
5
+ from app.models.schemas import Candidate
6
+
7
+ class MatchService:
8
+ def __init__(self):
9
+ self.pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
10
+ self.index_name = os.getenv("PINECONE_INDEX", "coderound")
11
+ self.index = self.pc.Index(self.index_name)
12
+
13
+ # Load local embedding model
14
+ model_name = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3")
15
+ self.model = SentenceTransformer(model_name)
16
+
17
+ self.top_k = int(os.getenv("STAGE2_TOP_K", "20"))
18
+
19
+ def get_embedding(self, text: str):
20
+ return self.model.encode(text).tolist()
21
+
22
+ async def get_top_candidates(self, jd: str, candidates: List[Candidate]) -> List[Candidate]:
23
+ # 1. Prepare vectors for batch upload
24
+ vectors = []
25
+ candidate_map = {}
26
+
27
+ for c in candidates:
28
+ # Combine fields for semantic weight
29
+ search_text = f"{c.name} {c.skills} {c.experience} {c.projects} {c.resume_text}"
30
+ embedding = self.get_embedding(search_text)
31
+
32
+ vectors.append({
33
+ "id": c.id,
34
+ "values": embedding,
35
+ "metadata": {"name": c.name, "email": c.email}
36
+ })
37
+ candidate_map[c.id] = c
38
+
39
+ # 2. Upsert to Pinecone
40
+ self.index.upsert(vectors=vectors)
41
+
42
+ # 3. Embed JD and Query
43
+ jd_embedding = self.get_embedding(jd)
44
+ query_results = self.index.query(
45
+ vector=jd_embedding,
46
+ top_k=self.top_k,
47
+ include_metadata=True
48
+ )
49
+
50
+ # 4. Map back to Candidate objects
51
+ top_candidates = []
52
+ for match in query_results.matches:
53
+ if match.id in candidate_map:
54
+ top_candidates.append(candidate_map[match.id])
55
+
56
+ return top_candidates
57
+
58
+ match_service = MatchService()
proj/backend/app/utils/__pycache__/groq_client.cpython-312.pyc ADDED
Binary file (1.77 kB). View file
 
proj/backend/app/utils/__pycache__/key_manager.cpython-312.pyc ADDED
Binary file (2 kB). View file
 
proj/backend/app/utils/groq_client.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from groq import AsyncGroq
4
+ from app.utils.key_manager import key_manager
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ async def get_groq_completion(messages: list, model: str = None) -> str:
10
+ if model is None:
11
+ model = os.getenv("GROQ_MODEL", "llama3-70b-8192")
12
+
13
+ max_retries = len(key_manager.keys)
14
+ last_error = None
15
+
16
+ for _ in range(max_retries):
17
+ try:
18
+ api_key = key_manager.get_next_key()
19
+ client = AsyncGroq(api_key=api_key)
20
+
21
+ response = await client.chat.completions.create(
22
+ messages=messages,
23
+ model=model,
24
+ temperature=0.7,
25
+ )
26
+ return response.choices[0].message.content
27
+ except Exception as e:
28
+ logger.warning(f"Error using key: {e}. Retrying with next key...")
29
+ last_error = e
30
+ continue
31
+
32
+ raise Exception(f"All API keys exhausted or failed. Last error: {last_error}")
proj/backend/app/utils/key_manager.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import threading
3
+ from typing import List
4
+
5
+ class KeyRotationManager:
6
+ def __init__(self):
7
+ keys_str = os.getenv("GROQ_API_KEYS")
8
+ if not keys_str:
9
+ keys_str = os.getenv("GROQ_API_KEY", "")
10
+
11
+ self.keys = [k.strip() for k in keys_str.split(",") if k.strip()]
12
+ print(f"KeyRotationManager initialized with {len(self.keys)} keys.")
13
+ if not self.keys:
14
+ print("WARNING: No GROQ_API_KEYS or GROQ_API_KEY found in environment!")
15
+ self.current_index = 0
16
+ self.lock = threading.Lock()
17
+
18
+ def get_next_key(self) -> str:
19
+ with self.lock:
20
+ if not self.keys:
21
+ raise ValueError("No GROQ_API_KEYS found in environment variables.")
22
+
23
+ key = self.keys[self.current_index]
24
+ self.current_index = (self.current_index + 1) % len(self.keys)
25
+ return key
26
+
27
+ key_manager = KeyRotationManager()
proj/backend/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-dotenv
4
+ pandas
5
+ groq
6
+ pydantic
7
+ python-multipart
8
+ asyncio
9
+ httpx
10
+ pinecone
11
+ sentence-transformers
12
+ numpy
proj/frontend/.env.local.example ADDED
@@ -0,0 +1 @@
 
 
1
+ NEXT_PUBLIC_API_URL=http://localhost:8000
proj/frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
proj/frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
proj/frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
proj/frontend/eslint.config.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+
4
+ const eslintConfig = defineConfig([
5
+ ...nextVitals,
6
+ // Override default ignores of eslint-config-next.
7
+ globalIgnores([
8
+ // Default ignores of eslint-config-next:
9
+ ".next/**",
10
+ "out/**",
11
+ "build/**",
12
+ "next-env.d.ts",
13
+ ]),
14
+ ]);
15
+
16
+ export default eslintConfig;
proj/frontend/jsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@/*": ["./src/*"]
5
+ }
6
+ }
7
+ }
proj/frontend/next.config.mjs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ /* config options here */
4
+ };
5
+
6
+ export default nextConfig;
proj/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
proj/frontend/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "16.2.3",
13
+ "react": "19.2.4",
14
+ "react-dom": "19.2.4",
15
+ "axios": "^1.6.7",
16
+ "lucide-react": "latest",
17
+ "framer-motion": "^11.0.8",
18
+ "clsx": "^2.1.0",
19
+ "tailwind-merge": "^2.2.1"
20
+ },
21
+ "devDependencies": {
22
+ "@tailwindcss/postcss": "^4",
23
+ "eslint": "^9",
24
+ "eslint-config-next": "16.2.3",
25
+ "tailwindcss": "^4"
26
+ }
27
+ }
proj/frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
proj/frontend/public/file.svg ADDED
proj/frontend/public/globe.svg ADDED
proj/frontend/public/next.svg ADDED
proj/frontend/public/vercel.svg ADDED
proj/frontend/public/window.svg ADDED
proj/frontend/src/app/favicon.ico ADDED
proj/frontend/src/app/globals.css ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #f8fafc;
5
+ --foreground: #0f172a;
6
+ --primary: #6366f1;
7
+ --glass: rgba(255, 255, 255, 0.7);
8
+ --glass-border: rgba(255, 255, 255, 0.3);
9
+ }
10
+
11
+ .dark {
12
+ --background: #020617;
13
+ --foreground: #f8fafc;
14
+ --primary: #818cf8;
15
+ --glass: rgba(15, 23, 42, 0.7);
16
+ --glass-border: rgba(30, 41, 59, 0.5);
17
+ }
18
+
19
+ body {
20
+ background-color: var(--background);
21
+ color: var(--foreground);
22
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
23
+ overflow-x: hidden;
24
+ }
25
+
26
+ .glass-card {
27
+ background: var(--glass);
28
+ backdrop-filter: blur(12px);
29
+ -webkit-backdrop-filter: blur(12px);
30
+ border: 1px solid var(--glass-border);
31
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
32
+ }
33
+
34
+ .gradient-text {
35
+ background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ }
39
+
40
+ .animate-in {
41
+ animation: fadeIn 0.5s ease-out forwards;
42
+ }
43
+
44
+ @keyframes fadeIn {
45
+ from { opacity: 0; transform: translateY(10px); }
46
+ to { opacity: 1; transform: translateY(0); }
47
+ }
48
+
49
+ /* Custom Scrollbar */
50
+ ::-webkit-scrollbar {
51
+ width: 6px;
52
+ }
53
+ ::-webkit-scrollbar-track {
54
+ background: transparent;
55
+ }
56
+ ::-webkit-scrollbar-thumb {
57
+ background: #cbd5e1;
58
+ border-radius: 10px;
59
+ }
60
+ .dark ::-webkit-scrollbar-thumb {
61
+ background: #334155;
62
+ }
proj/frontend/src/app/layout.jsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css';
2
+ import { Inter } from 'next/font/google';
3
+
4
+ const inter = Inter({ subsets: ['latin'] });
5
+
6
+ export const metadata = {
7
+ title: 'AI Recruitment Engine | Elevate Your Hiring',
8
+ description: 'Production-ready AI candidate ranking platform powered by Groq and multi-agent synergy.',
9
+ };
10
+
11
+ export default function RootLayout({ children }) {
12
+ return (
13
+ <html lang="en">
14
+ <body className={inter.className}>
15
+ <div className="fixed inset-0 -z-10 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-indigo-100 via-slate-50 to-white dark:from-indigo-950 dark:via-slate-950 dark:to-black"></div>
16
+ <nav className="sticky top-0 z-50 w-full glass-card border-b bg-white/50 dark:bg-black/50 backdrop-blur-md">
17
+ <div className="container mx-auto px-6 py-4 flex justify-between items-center">
18
+ <h1 className="text-2xl font-bold gradient-text">RecruitAI</h1>
19
+ <div className="flex gap-6 items-center">
20
+ <span className="text-sm font-medium text-slate-500 hover:text-indigo-600 cursor-pointer transition-colors">Dashboard</span>
21
+ <span className="text-sm font-medium text-slate-500 hover:text-indigo-600 cursor-pointer transition-colors">Analytics</span>
22
+ </div>
23
+ </div>
24
+ </nav>
25
+ <main className="min-h-screen">
26
+ {children}
27
+ </main>
28
+ <footer className="py-12 border-t border-slate-200 dark:border-slate-800 backdrop-blur-sm">
29
+ <div className="container mx-auto px-6 text-center text-slate-500 text-sm">
30
+ © 2024 AI Recruitment Engine. Built for the future of hiring.
31
+ </div>
32
+ </footer>
33
+ </body>
34
+ </html>
35
+ );
36
+ }
proj/frontend/src/app/page.jsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { Upload, Search, Zap, CheckCircle2, AlertCircle, Loader2, BarChart3, Users } from 'lucide-react';
6
+ import { uploadCSV, runEvaluation } from '@/lib/api';
7
+ import CandidateTable from '@/components/CandidateTable';
8
+ import CandidateDetail from '@/components/CandidateDetail';
9
+
10
+ export default function Dashboard() {
11
+ const [jd, setJd] = useState('');
12
+ const [candidates, setCandidates] = useState([]);
13
+ const [evaluations, setEvaluations] = useState([]);
14
+ const [isUploading, setIsUploading] = useState(false);
15
+ const [isEvaluating, setIsEvaluating] = useState(false);
16
+ const [selectedCandidate, setSelectedCandidate] = useState(null);
17
+
18
+ const handleFileUpload = async (e) => {
19
+ const file = e.target.files[0];
20
+ if (!file) return;
21
+
22
+ setIsUploading(true);
23
+ try {
24
+ const data = await uploadCSV(file);
25
+ setCandidates(data.candidates);
26
+ } catch (err) {
27
+ console.error(err);
28
+ alert('Failed to upload CSV. Check console for details.');
29
+ } finally {
30
+ setIsUploading(false);
31
+ }
32
+ };
33
+
34
+ const handleEvaluate = async () => {
35
+ if (!jd || candidates.length === 0) {
36
+ alert('Please provide a JD and upload candidates.');
37
+ return;
38
+ }
39
+
40
+ setIsEvaluating(true);
41
+ try {
42
+ const data = await runEvaluation(jd, candidates);
43
+ setEvaluations(data.shortlist);
44
+ // We can also store data.details if needed for immediate access
45
+ window.evalDetails = data.details;
46
+ } catch (err) {
47
+ console.error(err);
48
+ alert('Evaluation failed. Check backend/Groq keys.');
49
+ } finally {
50
+ setIsEvaluating(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className="container mx-auto px-6 py-12">
56
+ <header className="mb-12">
57
+ <motion.h2
58
+ initial={{ opacity: 0, x: -20 }}
59
+ animate={{ opacity: 1, x: 0 }}
60
+ className="text-4xl font-extrabold tracking-tight"
61
+ >
62
+ Candidate <span className="gradient-text">Ranking Platform</span>
63
+ </motion.h2>
64
+ <p className="text-slate-500 mt-2 text-lg">Intelligent multi-agent assessment for your next top hire.</p>
65
+ </header>
66
+
67
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
68
+ {/* Left Column: Input */}
69
+ <div className="lg:col-span-4 space-y-6">
70
+ <section className="glass-card p-6 rounded-2xl">
71
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
72
+ <Zap className="w-5 h-5 text-indigo-500" /> Job Description
73
+ </h3>
74
+ <textarea
75
+ className="w-full h-64 p-4 bg-white/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all resize-none text-sm"
76
+ placeholder="Paste the job description here..."
77
+ value={jd}
78
+ onChange={(e) => setJd(e.target.value)}
79
+ />
80
+ </section>
81
+
82
+ <section className="glass-card p-6 rounded-2xl">
83
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-indigo-500">
84
+ <Upload className="w-5 h-5" /> Candidate Data (CSV)
85
+ </h3>
86
+ <div className="relative group">
87
+ <input
88
+ type="file"
89
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
90
+ onChange={handleFileUpload}
91
+ accept=".csv"
92
+ />
93
+ <div className="border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl p-8 text-center group-hover:border-indigo-500 transition-colors">
94
+ {isUploading ? (
95
+ <Loader2 className="w-8 h-8 mx-auto animate-spin text-indigo-500" />
96
+ ) : (
97
+ <>
98
+ <Users className="w-8 h-8 mx-auto text-slate-400 group-hover:text-indigo-500 transition-colors mb-2" />
99
+ <p className="text-sm font-medium">Click or drag CSV</p>
100
+ <p className="text-xs text-slate-500 mt-1">{candidates.length > 0 ? `${candidates.length} candidates loaded` : 'Supports names, skills, email...'}</p>
101
+ </>
102
+ )}
103
+ </div>
104
+ </div>
105
+ </section>
106
+
107
+ <button
108
+ onClick={handleEvaluate}
109
+ disabled={isEvaluating || !jd || candidates.length === 0}
110
+ className={`w-full py-4 rounded-xl flex items-center justify-center gap-2 font-bold text-white transition-all shadow-lg ${
111
+ isEvaluating || !jd || candidates.length === 0
112
+ ? 'bg-slate-400 cursor-not-allowed'
113
+ : 'bg-indigo-600 hover:bg-indigo-700 hover:scale-[1.02] active:scale-[0.98]'
114
+ }`}
115
+ >
116
+ {isEvaluating ? (
117
+ <Loader2 className="w-5 h-5 animate-spin" />
118
+ ) : (
119
+ <BarChart3 className="w-5 h-5" />
120
+ )}
121
+ {isEvaluating ? 'Running AI Agents...' : 'Run Evaluation'}
122
+ </button>
123
+ </div>
124
+
125
+ {/* Right Column: Results */}
126
+ <div className="lg:col-span-8">
127
+ <div className="glass-card rounded-2xl min-h-[600px] relative overflow-hidden">
128
+ <AnimatePresence mode="wait">
129
+ {evaluations.length > 0 ? (
130
+ <motion.div
131
+ key="results"
132
+ initial={{ opacity: 0 }}
133
+ animate={{ opacity: 1 }}
134
+ exit={{ opacity: 0 }}
135
+ className="p-6"
136
+ >
137
+ <CandidateTable
138
+ evaluations={evaluations}
139
+ onViewDetail={(c) => setSelectedCandidate(c)}
140
+ />
141
+ </motion.div>
142
+ ) : (
143
+ <motion.div
144
+ key="empty"
145
+ initial={{ opacity: 0 }}
146
+ animate={{ opacity: 1 }}
147
+ exit={{ opacity: 0 }}
148
+ className="absolute inset-0 flex flex-col items-center justify-center text-slate-400 p-12 text-center"
149
+ >
150
+ <Search className="w-16 h-16 mb-4 opacity-20" />
151
+ <h3 className="text-xl font-medium">No Evaluations Yet</h3>
152
+ <p className="max-w-xs mt-2 text-sm">Upload a CSV, paste a JD, and hit Run to see the magic happen with Groq LLMs.</p>
153
+ </motion.div>
154
+ )}
155
+ </AnimatePresence>
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ {/* Detail Modal */}
161
+ <AnimatePresence>
162
+ {selectedCandidate && (
163
+ <CandidateDetail
164
+ evaluation={selectedCandidate}
165
+ onClose={() => setSelectedCandidate(null)}
166
+ />
167
+ )}
168
+ </AnimatePresence>
169
+ </div>
170
+ );
171
+ }
proj/frontend/src/components/CandidateDetail.jsx ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { motion } from 'framer-motion';
5
+ import { X, ShieldCheck, Zap, Briefcase, Cpu, TrendingUp, Info, Search } from 'lucide-react';
6
+
7
+ export default function CandidateDetail({ evaluation, onClose }) {
8
+ const [activeTab, setActiveTab] = useState('Synthesis');
9
+
10
+ const agents = [
11
+ { name: 'Synthesis', icon: ShieldCheck },
12
+ { name: 'Signal Extraction', icon: Zap },
13
+ { name: 'Explanation', icon: Info },
14
+ { name: 'Founder Eval', icon: TrendingUp },
15
+ { name: 'HR Agent', icon: Search },
16
+ { name: 'Tech Agent', icon: Cpu },
17
+ { name: 'Business Agent', icon: Briefcase },
18
+ ];
19
+
20
+ const getAgentContent = (name) => {
21
+ if (name === 'Synthesis') return evaluation.synthesis;
22
+ const agent = evaluation.agent_outputs.find(a => a.agent_name === name);
23
+ return agent ? agent.content : 'No output available for this agent.';
24
+ };
25
+
26
+ return (
27
+ <motion.div
28
+ initial={{ opacity: 0 }}
29
+ animate={{ opacity: 1 }}
30
+ exit={{ opacity: 0 }}
31
+ className="fixed inset-0 z-[100] bg-black/40 backdrop-blur-sm flex justify-end"
32
+ onClick={onClose}
33
+ >
34
+ <motion.div
35
+ initial={{ x: '100%' }}
36
+ animate={{ x: 0 }}
37
+ exit={{ x: '100%' }}
38
+ transition={{ type: 'spring', damping: 30, stiffness: 300 }}
39
+ className="w-full max-w-2xl bg-white dark:bg-slate-950 h-full shadow-2xl flex flex-col"
40
+ onClick={e => e.stopPropagation()}
41
+ >
42
+ {/* Header */}
43
+ <div className="p-8 border-b border-slate-100 dark:border-slate-800 flex justify-between items-start">
44
+ <div>
45
+ <span className="text-xs font-bold text-indigo-600 uppercase tracking-widest mb-1 block">Full Report</span>
46
+ <h2 className="text-3xl font-bold text-slate-900 dark:text-white">{evaluation.name}</h2>
47
+ <div className="flex items-center gap-4 mt-2">
48
+ <div className="flex items-center gap-1.5">
49
+ <div className="w-2 h-2 rounded-full bg-emerald-500"></div>
50
+ <span className="text-sm font-medium text-slate-500">{evaluation.decision}</span>
51
+ </div>
52
+ <div className="text-sm text-slate-400">Score: {Math.round(evaluation.final_score)}/100</div>
53
+ </div>
54
+ </div>
55
+ <button
56
+ onClick={onClose}
57
+ className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors"
58
+ >
59
+ <X className="w-6 h-6 text-slate-400" />
60
+ </button>
61
+ </div>
62
+
63
+ {/* Content Area */}
64
+ <div className="flex flex-1 overflow-hidden">
65
+ {/* Sidebar Tabs */}
66
+ <div className="w-16 md:w-48 border-r border-slate-100 dark:border-slate-900 bg-slate-50/50 dark:bg-slate-900/20 py-6">
67
+ {agents.map((agent) => {
68
+ const Icon = agent.icon;
69
+ const isActive = activeTab === agent.name;
70
+ return (
71
+ <button
72
+ key={agent.name}
73
+ onClick={() => setActiveTab(agent.name)}
74
+ className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium transition-all ${
75
+ isActive
76
+ ? 'text-indigo-600 bg-white dark:bg-slate-900 shadow-sm border-r-2 border-indigo-600'
77
+ : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-300'
78
+ }`}
79
+ >
80
+ <Icon className="w-5 h-5 flex-shrink-0" />
81
+ <span className="hidden md:block truncate">{agent.name}</span>
82
+ </button>
83
+ );
84
+ })}
85
+ </div>
86
+
87
+ {/* Main Content */}
88
+ <div className="flex-1 overflow-y-auto p-8">
89
+ <AnimatePresence mode="wait">
90
+ <motion.div
91
+ key={activeTab}
92
+ initial={{ opacity: 0, y: 10 }}
93
+ animate={{ opacity: 1, y: 0 }}
94
+ exit={{ opacity: 0, y: -10 }}
95
+ className="prose dark:prose-invert max-w-none"
96
+ >
97
+ <div className="mb-6 flex items-center justify-between">
98
+ <h3 className="text-xl font-bold m-0">{activeTab} Output</h3>
99
+ </div>
100
+
101
+ {activeTab === 'Synthesis' ? (
102
+ <div className="space-y-6">
103
+ <div className="p-5 bg-indigo-50 dark:bg-indigo-900/20 rounded-2xl border border-indigo-100 dark:border-indigo-800">
104
+ <h4 className="text-indigo-900 dark:text-indigo-300 font-bold mb-2">Final Summary</h4>
105
+ <p className="text-sm leading-relaxed text-indigo-800 dark:text-indigo-400">{evaluation.synthesis}</p>
106
+ </div>
107
+
108
+ <div className="grid grid-cols-2 gap-4">
109
+ <div className="p-4 bg-emerald-50 dark:bg-emerald-900/10 rounded-xl border border-emerald-100 dark:border-emerald-800/50">
110
+ <h4 className="text-emerald-700 dark:text-emerald-400 font-bold text-sm mb-3">Key Strengths</h4>
111
+ <ul className="space-y-2">
112
+ {evaluation.strengths.map((s, i) => (
113
+ <li key={i} className="text-xs flex items-start gap-2 text-emerald-800/70 dark:text-emerald-500/70">
114
+ <span className="block w-1 h-1 rounded-full bg-emerald-400 mt-1.5 flex-shrink-0"></span>
115
+ {s}
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ </div>
120
+ <div className="p-4 bg-rose-50 dark:bg-rose-900/10 rounded-xl border border-rose-100 dark:border-rose-800/50">
121
+ <h4 className="text-rose-700 dark:text-rose-400 font-bold text-sm mb-3">Potential Risks</h4>
122
+ <ul className="space-y-2">
123
+ {evaluation.risks.map((r, i) => (
124
+ <li key={i} className="text-xs flex items-start gap-2 text-rose-800/70 dark:text-rose-500/70">
125
+ <span className="block w-1 h-1 rounded-full bg-rose-400 mt-1.5 flex-shrink-0"></span>
126
+ {r}
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="grid grid-cols-5 gap-2">
134
+ {Object.entries(evaluation.scores).map(([key, val]) => (
135
+ <div key={key} className="p-3 bg-slate-50 dark:bg-slate-900 rounded-lg text-center">
136
+ <div className="text-[10px] uppercase text-slate-400 font-bold mb-1">{key}</div>
137
+ <div className="text-sm font-bold text-slate-700 dark:text-slate-300">{val}</div>
138
+ </div>
139
+ ))}
140
+ </div>
141
+ </div>
142
+ ) : (
143
+ <div className="bg-slate-50 dark:bg-slate-900/50 p-6 rounded-2xl whitespace-pre-wrap text-sm leading-relaxed text-slate-600 dark:text-slate-400 border border-slate-100 dark:border-slate-800">
144
+ {getAgentContent(activeTab)}
145
+ </div>
146
+ )}
147
+ </motion.div>
148
+ </AnimatePresence>
149
+ </div>
150
+ </div>
151
+ </motion.div>
152
+ </motion.div>
153
+ );
154
+ }
proj/frontend/src/components/CandidateTable.jsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { ChevronRight, ArrowUpRight, ShieldCheck, AlertTriangle, Target } from 'lucide-react';
5
+
6
+ export default function CandidateTable({ evaluations, onViewDetail }) {
7
+ if (!evaluations || evaluations.length === 0) return null;
8
+
9
+ return (
10
+ <div className="overflow-x-auto">
11
+ <table className="w-full text-left border-separate border-spacing-y-3">
12
+ <thead>
13
+ <tr className="text-slate-500 text-sm font-medium">
14
+ <th className="px-4 pb-2">Rank</th>
15
+ <th className="px-4 pb-2">Candidate</th>
16
+ <th className="px-4 pb-2">Score</th>
17
+ <th className="px-4 pb-2">Decision</th>
18
+ <th className="px-4 pb-2">Confidence</th>
19
+ <th className="px-4 pb-2 text-right">Action</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ {evaluations.map((evalItem, index) => (
24
+ <tr
25
+ key={evalItem.candidate_id}
26
+ className="glass-card hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-all group cursor-pointer"
27
+ onClick={() => onViewDetail(evalItem)}
28
+ >
29
+ <td className="px-4 py-4 rounded-l-xl">
30
+ <span className={`flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold ${
31
+ index === 0 ? 'bg-amber-100 text-amber-600' :
32
+ index === 1 ? 'bg-slate-100 text-slate-600' :
33
+ index === 2 ? 'bg-orange-100 text-orange-600' : 'bg-slate-50 text-slate-400'
34
+ }`}>
35
+ {index + 1}
36
+ </span>
37
+ </td>
38
+ <td className="px-4 py-4">
39
+ <div>
40
+ <div className="font-semibold text-slate-900 dark:text-white">{evalItem.name}</div>
41
+ <div className="text-xs text-slate-500 truncate max-w-[150px]">{evalItem.candidate_id}</div>
42
+ </div>
43
+ </td>
44
+ <td className="px-4 py-4">
45
+ <div className="flex items-center gap-2">
46
+ <div className="w-16 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden">
47
+ <div
48
+ className="h-full bg-indigo-500 transition-all duration-1000"
49
+ style={{ width: `${evalItem.final_score}%` }}
50
+ />
51
+ </div>
52
+ <span className="text-sm font-bold text-indigo-600">{Math.round(evalItem.final_score)}</span>
53
+ </div>
54
+ </td>
55
+ <td className="px-4 py-4">
56
+ <span className={`px-2 py-1 rounded-md text-[10px] uppercase font-bold tracking-wider ${
57
+ evalItem.decision === 'Strong Hire' ? 'bg-emerald-100 text-emerald-700' :
58
+ evalItem.decision === 'Hire' ? 'bg-blue-100 text-blue-700' :
59
+ evalItem.decision === 'Reject' ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700'
60
+ }`}>
61
+ {evalItem.decision}
62
+ </span>
63
+ </td>
64
+ <td className="px-4 py-4">
65
+ <div className="flex items-center gap-1.6 text-sm">
66
+ <Target className="w-4 h-4 text-slate-400" />
67
+ <span>{evalItem.confidence}%</span>
68
+ </div>
69
+ </td>
70
+ <td className="px-4 py-4 text-right rounded-r-xl">
71
+ <button className="p-2 hover:bg-white dark:hover:bg-slate-700 rounded-lg transition-colors">
72
+ <ChevronRight className="w-5 h-5 text-slate-400 group-hover:text-indigo-600" />
73
+ </button>
74
+ </td>
75
+ </tr>
76
+ ))}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ );
81
+ }
proj/frontend/src/lib/api.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
4
+
5
+ const api = axios.create({
6
+ baseURL: API_URL,
7
+ });
8
+
9
+ export const uploadCSV = async (file) => {
10
+ const formData = new FormData();
11
+ formData.append('file', file);
12
+ const response = await api.post('/upload-csv', formData);
13
+ return response.data;
14
+ };
15
+
16
+ export const runEvaluation = async (jd, candidates) => {
17
+ const response = await api.post('/evaluate', { jd, candidates });
18
+ return response.data;
19
+ };
20
+
21
+ export const getCandidateDetails = async (id) => {
22
+ const response = await api.get(`/candidate/${id}`);
23
+ return response.data;
24
+ };
25
+
26
+ export default api;
proj/frontend/src/lib/api.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
4
+
5
+ const api = axios.create({
6
+ baseURL: API_URL,
7
+ });
8
+
9
+ export const uploadCSV = async (file: File) => {
10
+ const formData = new FormData();
11
+ formData.append('file', file);
12
+ const response = await api.post('/upload-csv', formData);
13
+ return response.data;
14
+ };
15
+
16
+ export const runEvaluation = async (jd: string, candidates: any[]) => {
17
+ const response = await api.post('/evaluate', { jd, candidates });
18
+ return response.data;
19
+ };
20
+
21
+ export const getCandidateDetails = async (id: string) => {
22
+ const response = await api.get(`/candidate/${id}`);
23
+ return response.data;
24
+ };
25
+
26
+ export default api;