Saandraahh commited on
Commit
ad01d65
·
1 Parent(s): 01c8f1f

Pushing to IRIS_latest repository

Browse files
backend/api.py CHANGED
@@ -13,6 +13,7 @@ from supabase_ingest import process_resume
13
  from src.extraction.job_extractor import process_single_job
14
  from src.services.ats_service import analyze_ats_compatibility
15
  from src.services.analysis import identify_missing_skills, generate_ai_analysis
 
16
  from typing import Dict, Any, Optional, List
17
 
18
 
@@ -182,6 +183,8 @@ async def perform_candidate_analysis(candidate_id: str, job_id: str, force_refre
182
  "strengths": insights.get("strengths") or [],
183
  "weaknesses": insights.get("weaknesses") or [],
184
  "score": app_data.get("AI_score") or 0,
 
 
185
  "missing_skills": insights.get("missing_skills") or []
186
  }
187
 
@@ -190,6 +193,9 @@ async def perform_candidate_analysis(candidate_id: str, job_id: str, force_refre
190
  if not prof_resp.data:
191
  raise HTTPException(status_code=404, detail="Candidate not found")
192
 
 
 
 
193
  profile = prof_resp.data[0]
194
 
195
  # 2. Fetch Job Data
@@ -234,9 +240,10 @@ async def perform_candidate_analysis(candidate_id: str, job_id: str, force_refre
234
  insights = app_data.get("ai_insights") or {}
235
  return {
236
  "summary": app_data.get("ai_summary"),
237
- "strengths": insights.get("strengths") or [],
238
  "weaknesses": insights.get("weaknesses") or [],
239
  "score": app_data.get("AI_score") or 0,
 
 
240
  "missing_skills": missing
241
  }
242
 
@@ -258,11 +265,12 @@ async def perform_candidate_analysis(candidate_id: str, job_id: str, force_refre
258
  data_to_save = {
259
  "ai_summary": ai_insights.get("summary"),
260
  "ai_insights": {
261
- "strengths": ai_insights.get("strengths") or [],
262
  "weaknesses": ai_insights.get("weaknesses") or [],
263
- "missing_skills": missing
 
264
  },
265
- "AI_score": ai_insights.get("score") or 0
 
266
  }
267
  client.table("applications").update(data_to_save).eq("user_id", candidate_id).eq("job_id", job_id).execute()
268
  print(f"💾 Persisted AI analysis for candidate {candidate_id}")
@@ -274,6 +282,8 @@ async def perform_candidate_analysis(candidate_id: str, job_id: str, force_refre
274
  "strengths": ai_insights.get("strengths"),
275
  "weaknesses": ai_insights.get("weaknesses"),
276
  "score": ai_insights.get("score") or 0,
 
 
277
  "missing_skills": missing
278
  }
279
 
 
13
  from src.extraction.job_extractor import process_single_job
14
  from src.services.ats_service import analyze_ats_compatibility
15
  from src.services.analysis import identify_missing_skills, generate_ai_analysis
16
+ from src.matching.similarity import calculate_granular_match_score
17
  from typing import Dict, Any, Optional, List
18
 
19
 
 
183
  "strengths": insights.get("strengths") or [],
184
  "weaknesses": insights.get("weaknesses") or [],
185
  "score": app_data.get("AI_score") or 0,
186
+ "semantic_score": app_data.get("semantic_score") or 0,
187
+ "score_breakdown": insights.get("score_breakdown") or {},
188
  "missing_skills": insights.get("missing_skills") or []
189
  }
190
 
 
193
  if not prof_resp.data:
194
  raise HTTPException(status_code=404, detail="Candidate not found")
195
 
196
+ # 0.5 Granular Match Score (Vector Similarity)
197
+ semantic_result = await calculate_granular_match_score(client, candidate_id, job_id)
198
+
199
  profile = prof_resp.data[0]
200
 
201
  # 2. Fetch Job Data
 
240
  insights = app_data.get("ai_insights") or {}
241
  return {
242
  "summary": app_data.get("ai_summary"),
 
243
  "weaknesses": insights.get("weaknesses") or [],
244
  "score": app_data.get("AI_score") or 0,
245
+ "semantic_score": semantic_result.get("total_score"),
246
+ "score_breakdown": semantic_result.get("breakdown"),
247
  "missing_skills": missing
248
  }
249
 
 
265
  data_to_save = {
266
  "ai_summary": ai_insights.get("summary"),
267
  "ai_insights": {
 
268
  "weaknesses": ai_insights.get("weaknesses") or [],
269
+ "missing_skills": missing,
270
+ "score_breakdown": semantic_result.get("breakdown")
271
  },
272
+ "AI_score": ai_insights.get("score") or 0,
273
+ "semantic_score": semantic_result.get("total_score")
274
  }
275
  client.table("applications").update(data_to_save).eq("user_id", candidate_id).eq("job_id", job_id).execute()
276
  print(f"💾 Persisted AI analysis for candidate {candidate_id}")
 
282
  "strengths": ai_insights.get("strengths"),
283
  "weaknesses": ai_insights.get("weaknesses"),
284
  "score": ai_insights.get("score") or 0,
285
+ "semantic_score": semantic_result.get("total_score"),
286
+ "score_breakdown": semantic_result.get("breakdown"),
287
  "missing_skills": missing
288
  }
289
 
backend/src/embeddings/debug_embedding_storage.py CHANGED
@@ -6,10 +6,12 @@ import time
6
  # Add 'backend' directory to path so we can import 'supabase_ingest' directly
7
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
8
 
9
- from supabase_ingest import safe_generate_and_store_embeddings, client
 
10
 
 
11
  # Mock data
12
- user_id = "test_user_debug_123"
13
  extracted_data = {
14
  "headline": "Debug Engineer",
15
  "summary": "This is a test summary for debugging.",
@@ -28,7 +30,8 @@ try:
28
  profile_payload = {
29
  "id": user_id,
30
  "full_name": "Debug User",
31
- "email": "debug@example.com",
 
32
  "updated_at": "now()",
33
  # Add the fields we expect to be there
34
  "headline": extracted_data["headline"],
 
6
  # Add 'backend' directory to path so we can import 'supabase_ingest' directly
7
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
8
 
9
+ from src.embeddings.local_embedder import safe_generate_and_store_embeddings
10
+ from supabase_ingest import client
11
 
12
+ import uuid
13
  # Mock data
14
+ user_id = str(uuid.uuid4())
15
  extracted_data = {
16
  "headline": "Debug Engineer",
17
  "summary": "This is a test summary for debugging.",
 
30
  profile_payload = {
31
  "id": user_id,
32
  "full_name": "Debug User",
33
+ "email": f"debug_{int(time.time())}@example.com",
34
+ "phone": "+919876543210",
35
  "updated_at": "now()",
36
  # Add the fields we expect to be there
37
  "headline": extracted_data["headline"],
backend/src/embeddings/debug_match_score.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+ import asyncio
5
+ import uuid
6
+ import time
7
+
8
+ # Add root to path
9
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
10
+
11
+ from supabase_ingest import client
12
+ from src.embeddings.local_embedder import safe_generate_and_store_embeddings
13
+ from src.embeddings.job_embed import safe_generate_and_store_job_embeddings
14
+ from src.extraction.job_extractor import upsert_job_entities
15
+ from src.matching.similarity import calculate_granular_match_score
16
+
17
+ async def test_full_ranking_flow():
18
+ print("🚀 Testing Full Ranking Flow...")
19
+
20
+ # 1. Create Test Candidate
21
+ candidate_id = str(uuid.uuid4())
22
+ print(f"👤 Creating Test Candidate: {candidate_id}")
23
+ profile_payload = {
24
+ "id": candidate_id,
25
+ "full_name": "Ranking Test User",
26
+ "email": f"rank_test_{int(time.time())}@example.com",
27
+ "phone": "+919876543210",
28
+ "skills": "Python, Machine Learning, SQL",
29
+ "technical_skills": "PyTorch, Scikit-learn, PostgreSQL",
30
+ "experience_years": "5",
31
+ "role": "Data Scientist",
32
+ "updated_at": "now()"
33
+ }
34
+ client.table("profiles").upsert(profile_payload).execute()
35
+
36
+ # 2. Create Test Job
37
+ job_id = str(uuid.uuid4())
38
+ print(f"💼 Creating Test Job: {job_id}")
39
+ job_payload = {
40
+ "id": job_id,
41
+ "title": "Senior Data Scientist",
42
+ "description": "Looking for a Data Scientist with Python and AI experience.",
43
+ "updated_at": "now()"
44
+ }
45
+ client.table("jobs").upsert(job_payload).execute()
46
+
47
+ # Manually upsert entities for the job
48
+ job_entities = {
49
+ "skills": ["Python", "Problem Solving"],
50
+ "technical_skills": ["Machine Learning", "PyTorch"],
51
+ "experience": "5+ years of experience in data science.",
52
+ "certifications": []
53
+ }
54
+ upsert_job_entities(client, job_id, "Senior", job_entities)
55
+
56
+ # 3. Generate Embeddings
57
+ print("🧬 Generating Embeddings (On CPU)...")
58
+ safe_generate_and_store_embeddings(client, candidate_id)
59
+ safe_generate_and_store_job_embeddings(client, job_id)
60
+
61
+ # 4. Calculate Match Score
62
+ print("📊 Calculating Granular Match Score...")
63
+ result = await calculate_granular_match_score(client, candidate_id, job_id)
64
+
65
+ print("\n✅ MATCH SCORE RESULT:")
66
+ print(f"Total Score: {result.get('total_score')}%")
67
+ print(f"Breakdown: {result.get('breakdown')}")
68
+
69
+ if result.get('total_score', 0) > 0:
70
+ print("\n🎉 SUCCESS: Granular match score works!")
71
+ else:
72
+ print("\n❌ FAILURE: Total score is 0.")
73
+
74
+ if __name__ == "__main__":
75
+ asyncio.run(test_full_ranking_flow())
backend/src/embeddings/job_embed.py CHANGED
@@ -56,7 +56,7 @@ def safe_generate_and_store_job_embeddings(client, job_id: str) -> None:
56
  print(f"🧬 Generating job embeddings for Job: {job_id}")
57
 
58
  # 1. Fetch job entities
59
- resp = client.table("job_entities") \
60
  .select("*") \
61
  .eq("job_id", job_id) \
62
  .execute()
 
56
  print(f"🧬 Generating job embeddings for Job: {job_id}")
57
 
58
  # 1. Fetch job entities
59
+ resp = client.table("jobs_entities") \
60
  .select("*") \
61
  .eq("job_id", job_id) \
62
  .execute()
backend/src/embeddings/process_all_profiles.py CHANGED
@@ -6,7 +6,8 @@ import time
6
  # Add backend to path
7
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
8
 
9
- from supabase_ingest import client, safe_generate_and_store_embeddings
 
10
 
11
  def process_all_profiles():
12
  print("🔍 Fetching all user IDs from 'profiles' table...")
 
6
  # Add backend to path
7
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
8
 
9
+ from supabase_ingest import client
10
+ from src.embeddings.local_embedder import safe_generate_and_store_embeddings
11
 
12
  def process_all_profiles():
13
  print("🔍 Fetching all user IDs from 'profiles' table...")
backend/src/extraction/job_extractor.py CHANGED
@@ -8,6 +8,7 @@ from google import genai
8
  from google.genai import types
9
 
10
  from supabase import create_client
 
11
 
12
  # ------------------ CONFIGURATION ------------------
13
  RAW_DIR = "data/jobs/raw"
@@ -118,6 +119,13 @@ def upsert_job_entities(sb, job_id: str, experience_level: str, data: Dict[str,
118
  try:
119
  sb.table("jobs_entities").upsert(payload).execute()
120
  print(f"✅ Database updated for Job ID: {job_id}")
 
 
 
 
 
 
 
121
  except Exception as e:
122
  print(f"❌ DB Upsert Error for {job_id}: {e}")
123
 
 
8
  from google.genai import types
9
 
10
  from supabase import create_client
11
+ from src.embeddings.job_embed import safe_generate_and_store_job_embeddings
12
 
13
  # ------------------ CONFIGURATION ------------------
14
  RAW_DIR = "data/jobs/raw"
 
119
  try:
120
  sb.table("jobs_entities").upsert(payload).execute()
121
  print(f"✅ Database updated for Job ID: {job_id}")
122
+
123
+ # ⚠️ NEW: Generate Embeddings
124
+ try:
125
+ safe_generate_and_store_job_embeddings(sb, job_id)
126
+ except Exception as e:
127
+ print(f"⚠️ Job embedding generation failed (non-critical): {e}")
128
+
129
  except Exception as e:
130
  print(f"❌ DB Upsert Error for {job_id}: {e}")
131
 
backend/src/matching/similarity.py CHANGED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import numpy as np
3
+ from typing import Dict, Any, List
4
+ from supabase import Client
5
+
6
+ def cosine_similarity(v1: List[float], v2: List[float]) -> float:
7
+ """Calculates cosine similarity between two vectors."""
8
+ if not v1 or not v2 or len(v1) != len(v2):
9
+ return 0.0
10
+
11
+ a = np.array(v1)
12
+ b = np.array(v2)
13
+
14
+ # Check if vectors are zero vectors
15
+ if np.all(a == 0) or np.all(b == 0):
16
+ return 0.0
17
+
18
+ dot_product = np.dot(a, b)
19
+ norm_a = np.linalg.norm(a)
20
+ norm_b = np.linalg.norm(b)
21
+
22
+ if norm_a == 0 or norm_b == 0:
23
+ return 0.0
24
+
25
+ return float(dot_product / (norm_a * norm_b))
26
+
27
+ async def calculate_granular_match_score(client: Client, candidate_id: str, job_id: str) -> Dict[str, Any]:
28
+ """
29
+ Fetches embeddings for candidate and job, calculates entity-wise similarity,
30
+ and returns a weighted total score.
31
+ """
32
+ print(f"📊 Calculating granular match score for Candidate: {candidate_id}, Job: {job_id}")
33
+
34
+ # 1. Fetch Embeddings
35
+ try:
36
+ profile_resp = client.table("profile_embeddings").select("*").eq("id", candidate_id).execute()
37
+ job_resp = client.table("job_embeddings").select("*").eq("job_id", job_id).execute()
38
+
39
+ if not profile_resp.data:
40
+ print(f"⚠️ No profile embeddings found for {candidate_id}")
41
+ return {"total_score": 0, "breakdown": {}, "error": "Profile embeddings missing"}
42
+
43
+ if not job_resp.data:
44
+ print(f"⚠️ No job embeddings found for {job_id}")
45
+ return {"total_score": 0, "breakdown": {}, "error": "Job embeddings missing"}
46
+
47
+ profile_emb = profile_resp.data[0]
48
+ job_emb = job_resp.data[0]
49
+
50
+ except Exception as e:
51
+ print(f"❌ Database error in match score: {e}")
52
+ return {"total_score": 0, "breakdown": {}, "error": str(e)}
53
+
54
+ # 2. Define Weights
55
+ # These could eventually be user-defined
56
+ WEIGHTS = {
57
+ "skills": 0.35,
58
+ "technical_skills": 0.35,
59
+ "experience": 0.20,
60
+ "certifications": 0.10
61
+ }
62
+
63
+ # 3. Calculate Individual Similarities
64
+ scores = {}
65
+
66
+ # Skill matching
67
+ scores["skills"] = cosine_similarity(profile_emb.get("skills"), job_emb.get("skills"))
68
+ scores["technical_skills"] = cosine_similarity(profile_emb.get("technical_skills"), job_emb.get("technical_skills"))
69
+
70
+ # Experience matching
71
+ scores["experience"] = cosine_similarity(profile_emb.get("experience"), job_emb.get("experience"))
72
+
73
+ # Certifications matching
74
+ scores["certifications"] = cosine_similarity(profile_emb.get("certifications"), job_emb.get("certifications"))
75
+
76
+ # 4. Calculate Weighted Total
77
+ total_score = 0
78
+ available_weight = 0
79
+
80
+ for key, weight in WEIGHTS.items():
81
+ if scores.get(key) is not None:
82
+ total_score += scores[key] * weight
83
+ available_weight += weight
84
+
85
+ # Normalize if some fields were missing (though WEIGHTS sums to 1.0)
86
+ if available_weight > 0:
87
+ final_score = (total_score / available_weight) * 100
88
+ else:
89
+ final_score = 0
90
+
91
+ return {
92
+ "total_score": round(final_score, 1),
93
+ "breakdown": {k: round(v * 100, 1) for k, v in scores.items()},
94
+ "weights": WEIGHTS
95
+ }
backend/supabase_ingest.py CHANGED
@@ -19,6 +19,7 @@ from supabase import create_client
19
 
20
  # ✅ CORRECT IMPORT based on your file structure
21
  from src.extraction.person_details_extraction_gemini import process_single_resume
 
22
 
23
  # ---------------------------------------------------------------------
24
  # ENV SETUP
@@ -235,7 +236,13 @@ def process_resume(client, user_id: str, file_path: str, temp_dir: str = "data/r
235
  payload = build_resume_payload(user_id, extracted_data, file_path, file_hash)
236
  upsert_profile(client, payload)
237
 
238
- # 5. Cleanup
 
 
 
 
 
 
239
  if os.path.exists(local_path):
240
  os.remove(local_path)
241
 
@@ -299,6 +306,12 @@ def main():
299
  # 6. Upsert to DB
300
  upsert_profile(client, payload)
301
 
 
 
 
 
 
 
302
  except Exception as e:
303
  print(f" ❌ Pipeline failed for this file: {e}")
304
 
 
19
 
20
  # ✅ CORRECT IMPORT based on your file structure
21
  from src.extraction.person_details_extraction_gemini import process_single_resume
22
+ from src.embeddings.local_embedder import safe_generate_and_store_embeddings
23
 
24
  # ---------------------------------------------------------------------
25
  # ENV SETUP
 
236
  payload = build_resume_payload(user_id, extracted_data, file_path, file_hash)
237
  upsert_profile(client, payload)
238
 
239
+ # 5. Generate Embeddings
240
+ try:
241
+ safe_generate_and_store_embeddings(client, user_id)
242
+ except Exception as e:
243
+ print(f"⚠️ Embedding generation failed (non-critical): {e}")
244
+
245
+ # 6. Cleanup
246
  if os.path.exists(local_path):
247
  os.remove(local_path)
248
 
 
306
  # 6. Upsert to DB
307
  upsert_profile(client, payload)
308
 
309
+ # 7. Generate Embeddings
310
+ try:
311
+ safe_generate_and_store_embeddings(client, user_id)
312
+ except Exception as e:
313
+ print(f" ⚠️ Embedding generation failed (non-critical): {e}")
314
+
315
  except Exception as e:
316
  print(f" ❌ Pipeline failed for this file: {e}")
317
 
backend/update_applications_table.sql ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+
2
+ -- Update applications table to include semantic match score results
3
+ ALTER TABLE applications
4
+ ADD COLUMN IF NOT EXISTS semantic_score FLOAT,
5
+ ADD COLUMN IF NOT EXISTS score_breakdown JSONB;
6
+
7
+ -- Add comments for documentation
8
+ COMMENT ON COLUMN applications.semantic_score IS 'Overall semantic match score calculated via embeddings';
9
+ COMMENT ON COLUMN applications.score_breakdown IS 'Breakdown of entity-wise similarity scores';
src/components/CandidateDrawer.jsx CHANGED
@@ -132,17 +132,45 @@ const CandidateDrawer = ({ isOpen, onClose, candidate }) => {
132
 
133
  <div style={{ marginBottom: '2rem', padding: '1rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.75rem', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
134
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
135
- <span style={{ fontWeight: 'bold', color: '#EF4444' }}>{analysis?.score !== undefined ? 'AI Score' : 'Semantic Match Score'}</span>
136
- <span style={{ fontWeight: 'bold', color: 'white' }}>{analysis?.score ?? (candidate.score || candidate.matchScore || 0)}%</span>
137
  </div>
138
- <div style={{ width: '100%', height: '6px', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: '3px' }}>
139
  <motion.div
140
  initial={{ width: 0 }}
141
- animate={{ width: `${analysis?.score ?? (candidate.score || candidate.matchScore || 0)}%` }}
142
  transition={{ delay: 0.2, duration: 1 }}
143
  style={{ height: '100%', backgroundColor: '#EF4444', borderRadius: '3px', boxShadow: '0 0 10px rgba(239,68,68,0.5)' }}
144
  />
145
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </div>
147
 
148
  {loadingAnalysis && (
 
132
 
133
  <div style={{ marginBottom: '2rem', padding: '1rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.75rem', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
134
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
135
+ <span style={{ fontWeight: 'bold', color: '#EF4444' }}>AI Score</span>
136
+ <span style={{ fontWeight: 'bold', color: 'white' }}>{analysis?.score ?? (candidate.score || 0)}%</span>
137
  </div>
138
+ <div style={{ width: '100%', height: '6px', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: '3px', marginBottom: '1rem' }}>
139
  <motion.div
140
  initial={{ width: 0 }}
141
+ animate={{ width: `${analysis?.score ?? (candidate.score || 0)}%` }}
142
  transition={{ delay: 0.2, duration: 1 }}
143
  style={{ height: '100%', backgroundColor: '#EF4444', borderRadius: '3px', boxShadow: '0 0 10px rgba(239,68,68,0.5)' }}
144
  />
145
  </div>
146
+
147
+ {analysis?.semantic_score !== undefined && (
148
+ <>
149
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
150
+ <span style={{ fontWeight: 'bold', color: '#38bdf8' }}>Semantic Match Score</span>
151
+ <span style={{ fontWeight: 'bold', color: 'white' }}>{analysis.semantic_score}%</span>
152
+ </div>
153
+ <div style={{ width: '100%', height: '6px', backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: '3px', marginBottom: '1rem' }}>
154
+ <motion.div
155
+ initial={{ width: 0 }}
156
+ animate={{ width: `${analysis.semantic_score}%` }}
157
+ transition={{ delay: 0.3, duration: 1 }}
158
+ style={{ height: '100%', backgroundColor: '#38bdf8', borderRadius: '3px', boxShadow: '0 0 10px rgba(56,189,248,0.5)' }}
159
+ />
160
+ </div>
161
+
162
+ {analysis.score_breakdown && (
163
+ <div style={{ marginTop: '0.5rem', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
164
+ {Object.entries(analysis.score_breakdown).map(([entity, score]) => (
165
+ <div key={entity} style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'flex', justifyContent: 'space-between' }}>
166
+ <span style={{ textTransform: 'capitalize' }}>{entity.replace('_', ' ')}</span>
167
+ <span style={{ color: 'white' }}>{score}%</span>
168
+ </div>
169
+ ))}
170
+ </div>
171
+ )}
172
+ </>
173
+ )}
174
  </div>
175
 
176
  {loadingAnalysis && (
src/components/Icons.jsx CHANGED
@@ -2,34 +2,34 @@ import React from 'react';
2
  import { motion } from 'framer-motion';
3
 
4
  // --- EXISTING ICONS (Do not change these) ---
5
- export const LogoutIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V5h10a1 1 0 100-2H3zm12.293 4.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L16.586 13H9a1 1 0 110-2h7.586l-1.293-1.293a1 1 0 010-1.414z" clipRule="evenodd" /></svg> );
6
- export const BriefcaseIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 2a2 2 0 00-2 2v1H6a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2V4a2 2 0 00-2-2zm-2 2V4h4v1H8z" clipRule="evenodd" /></svg> );
7
- export const UserCircleIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0012 11z" clipRule="evenodd" /></svg> );
8
- export const ChatIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path d="M2 5a2 2 0 012-2h12a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm1.5 0a.5.5 0 00-.5.5v6a.5.5 0 00.5.5h11a.5.5 0 00.5-.5V5.5a.5.5 0 00-.5-.5h-11z" /><path d="M8 8a1 1 0 100-2 1 1 0 000 2zm4 0a1 1 0 100-2 1 1 0 000 2zm-2 1a1 1 0 11-2 0 1 1 0 012 0z" /></svg> );
9
- export const CalendarIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg> );
10
  export const UploadIcon = () => <svg style={{ width: '32px', height: '32px', color: 'rgba(255,255,255,0.5)', marginBottom: '0.5rem' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>;
11
- export const SpinnerIcon = () => <motion.svg animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></motion.svg>;
12
- export const SearchIcon = () => ( <svg style={{ width: '20px', height: '20px', color: 'rgba(255, 255, 255, 0.5)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" /></svg> );
13
- export const AtsCheckerIcon = () => ( <svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg> );
14
- export const FilterIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg> );
15
- export const ScoringIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg> );
16
- export const ClearIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg> );
17
- export const ViewIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg> );
18
- export const ChevronLeftIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> );
19
- export const ChevronRightIcon = () => ( <svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg> );
20
- export const CheckSquareIcon = () => ( <svg style={{ width: '18px', height: '18px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg> );
21
- export const MailIcon = () => ( <svg style={{ width: '18px', height: '18px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg> );
22
- export const LoaderIcon = () => ( <svg style={{ width: '24px', height: '24px', animation: 'spin 1s linear infinite' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg> );
23
 
24
  // Animated Chevron for Dropdowns
25
- export const ChevronDownIcon = ({ isOpen }) => (
26
- <motion.svg
27
- animate={{ rotate: isOpen ? 180 : 0 }}
28
- style={{ width: '16px', height: '16px' }}
29
- viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
30
- >
31
- <polyline points="6 9 12 15 18 9"></polyline>
32
- </motion.svg>
33
  );
34
 
35
 
@@ -76,7 +76,7 @@ export const RefreshIcon = ({ className }) => (
76
 
77
  export const EmptyStateIcon = ({ className }) => (
78
  <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
79
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
80
  </svg>
81
  );
82
 
 
2
  import { motion } from 'framer-motion';
3
 
4
  // --- EXISTING ICONS (Do not change these) ---
5
+ export const LogoutIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V5h10a1 1 0 100-2H3zm12.293 4.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L16.586 13H9a1 1 0 110-2h7.586l-1.293-1.293a1 1 0 010-1.414z" clipRule="evenodd" /></svg>);
6
+ export const BriefcaseIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 2a2 2 0 00-2 2v1H6a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2V4a2 2 0 00-2-2zm-2 2V4h4v1H8z" clipRule="evenodd" /></svg>);
7
+ export const UserCircleIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0012 11z" clipRule="evenodd" /></svg>);
8
+ export const ChatIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path d="M2 5a2 2 0 012-2h12a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm1.5 0a.5.5 0 00-.5.5v6a.5.5 0 00.5.5h11a.5.5 0 00.5-.5V5.5a.5.5 0 00-.5-.5h-11z" /><path d="M8 8a1 1 0 100-2 1 1 0 000 2zm4 0a1 1 0 100-2 1 1 0 000 2zm-2 1a1 1 0 11-2 0 1 1 0 012 0z" /></svg>);
9
+ export const CalendarIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
10
  export const UploadIcon = () => <svg style={{ width: '32px', height: '32px', color: 'rgba(255,255,255,0.5)', marginBottom: '0.5rem' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>;
11
+ export const SpinnerIcon = () => <motion.svg animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></motion.svg>;
12
+ export const SearchIcon = () => (<svg style={{ width: '20px', height: '20px', color: 'rgba(255, 255, 255, 0.5)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" /></svg>);
13
+ export const AtsCheckerIcon = () => (<svg style={{ width: '16px', height: '16px', marginRight: '8px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>);
14
+ export const FilterIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>);
15
+ export const ScoringIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>);
16
+ export const ClearIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>);
17
+ export const ViewIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>);
18
+ export const ChevronLeftIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>);
19
+ export const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>);
20
+ export const CheckSquareIcon = () => (<svg style={{ width: '18px', height: '18px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>);
21
+ export const MailIcon = () => (<svg style={{ width: '18px', height: '18px' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>);
22
+ export const LoaderIcon = () => (<svg style={{ width: '24px', height: '24px', animation: 'spin 1s linear infinite' }} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg>);
23
 
24
  // Animated Chevron for Dropdowns
25
+ export const ChevronDownIcon = ({ isOpen }) => (
26
+ <motion.svg
27
+ animate={{ rotate: isOpen ? 180 : 0 }}
28
+ style={{ width: '16px', height: '16px' }}
29
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
30
+ >
31
+ <polyline points="6 9 12 15 18 9"></polyline>
32
+ </motion.svg>
33
  );
34
 
35
 
 
76
 
77
  export const EmptyStateIcon = ({ className }) => (
78
  <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
79
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
80
  </svg>
81
  );
82