Muhammed Sameer commited on
Commit
ff85727
·
1 Parent(s): 59f9574

new feature implemented

Browse files
backend/api.py CHANGED
@@ -5,7 +5,7 @@ from dotenv import load_dotenv
5
  # Load env BEFORE importing modules that depend on it
6
  load_dotenv()
7
 
8
- from fastapi import FastAPI, HTTPException, UploadFile, Form, File
9
  from pydantic import BaseModel
10
  from supabase import create_client
11
  from fastapi.middleware.cors import CORSMiddleware
@@ -14,6 +14,7 @@ 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
 
@@ -137,6 +138,32 @@ async def jobs_webhook(request: WebhookRequest):
137
  print(f"❌ Job processing failed: {e}")
138
  raise HTTPException(status_code=500, detail=str(e))
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  @app.post("/analyze-ats")
141
  async def analyze_ats_endpoint(
142
  resume: UploadFile = File(...),
 
5
  # Load env BEFORE importing modules that depend on it
6
  load_dotenv()
7
 
8
+ from fastapi import FastAPI, HTTPException, UploadFile, Form, File, BackgroundTasks
9
  from pydantic import BaseModel
10
  from supabase import create_client
11
  from fastapi.middleware.cors import CORSMiddleware
 
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 src.services.clustering_service import ClusteringService
18
  from typing import Dict, Any, Optional, List
19
 
20
 
 
138
  print(f"❌ Job processing failed: {e}")
139
  raise HTTPException(status_code=500, detail=str(e))
140
 
141
+ # --- CLUSTERING ENDPOINT ---
142
+ @app.post("/run-clustering")
143
+ async def run_clustering_endpoint(background_tasks: BackgroundTasks):
144
+ """
145
+ Trigger candidate clustering asynchronously.
146
+ Returns immediately while clustering runs in the background.
147
+ """
148
+ print("🚀 Starting clustering pipeline...")
149
+
150
+ def run_clustering():
151
+ """Background task to run clustering."""
152
+ try:
153
+ service = ClusteringService()
154
+ service.run_clustering_pipeline(n_clusters=5)
155
+ print("✅ Clustering pipeline completed successfully!")
156
+ except Exception as e:
157
+ print(f"❌ Clustering failed: {e}")
158
+
159
+ # Add to background tasks
160
+ background_tasks.add_task(run_clustering)
161
+
162
+ return {
163
+ "status": "started",
164
+ "message": "Clustering pipeline started. This may take a few minutes. Refresh the page to see updated clusters."
165
+ }
166
+
167
  @app.post("/analyze-ats")
168
  async def analyze_ats_endpoint(
169
  resume: UploadFile = File(...),
src/components/Admin/AdminInterviewManagement.jsx CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
- import CandidateDrawer from '../CandidateDrawer';
5
  import ScheduleInterviewModal from '../ScheduleInterviewModal';
6
 
7
  // --- ICONS ---
@@ -454,10 +454,9 @@ export default function AdminInterviewManagement() {
454
 
455
  <AnimatePresence>
456
  {isDrawerOpen && (
457
- <CandidateDrawer
458
- isOpen={isDrawerOpen}
459
- onClose={() => setIsDrawerOpen(false)}
460
  candidate={drawerCandidate}
 
461
  />
462
  )}
463
  </AnimatePresence>
 
1
  import React, { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { supabase } from '../../supabaseClient';
4
+ import FullProfileOverlay from '../FullProfileOverlay';
5
  import ScheduleInterviewModal from '../ScheduleInterviewModal';
6
 
7
  // --- ICONS ---
 
454
 
455
  <AnimatePresence>
456
  {isDrawerOpen && (
457
+ <FullProfileOverlay
 
 
458
  candidate={drawerCandidate}
459
+ onClose={() => setIsDrawerOpen(false)}
460
  />
461
  )}
462
  </AnimatePresence>
src/components/Admin/TalentClusters.jsx CHANGED
@@ -323,6 +323,7 @@ export default function TalentClusters() {
323
  const [searchQuery, setSearchQuery] = useState('');
324
  const [selectedProfile, setSelectedProfile] = useState(null);
325
  const [error, setError] = useState(null);
 
326
 
327
  useEffect(() => {
328
  fetchClusters();
@@ -356,6 +357,54 @@ export default function TalentClusters() {
356
  }
357
  };
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  const clusterEntries = Object.entries(clusters).sort((a, b) => b[1].length - a[1].length);
360
  const totalProfiles = Object.values(clusters).reduce((s, arr) => s + arr.length, 0);
361
 
@@ -418,16 +467,25 @@ export default function TalentClusters() {
418
  />
419
  </div>
420
  <motion.button
421
- onClick={fetchClusters}
422
- whileHover={{ scale: 1.04 }}
423
- whileTap={{ scale: 0.96 }}
 
424
  style={{
425
- backgroundColor: 'rgba(239,68,68,0.15)', border: '1px solid rgba(239,68,68,0.4)',
426
- color: '#EF4444', padding: '0.75rem 1.25rem', borderRadius: '0.5rem',
427
- cursor: 'pointer', fontWeight: '600', fontSize: '0.85rem', whiteSpace: 'nowrap'
 
 
 
 
 
 
 
 
428
  }}
429
  >
430
- Refresh
431
  </motion.button>
432
  </div>
433
 
 
323
  const [searchQuery, setSearchQuery] = useState('');
324
  const [selectedProfile, setSelectedProfile] = useState(null);
325
  const [error, setError] = useState(null);
326
+ const [isClusteringRunning, setIsClusteringRunning] = useState(false);
327
 
328
  useEffect(() => {
329
  fetchClusters();
 
357
  }
358
  };
359
 
360
+ const runClustering = async () => {
361
+ setIsClusteringRunning(true);
362
+ try {
363
+ const response = await fetch('http://localhost:8000/run-clustering', {
364
+ method: 'POST',
365
+ headers: { 'Content-Type': 'application/json' }
366
+ });
367
+
368
+ if (!response.ok) {
369
+ throw new Error('Failed to start clustering');
370
+ }
371
+
372
+ const data = await response.json();
373
+ console.log('✅ Clustering started:', data);
374
+ alert('Clustering pipeline started! This may take a few minutes. You will be notified when complete.');
375
+
376
+ // Poll for completion (check every 5 seconds for up to 5 minutes)
377
+ const pollInterval = setInterval(async () => {
378
+ try {
379
+ await fetchClusters();
380
+ // If we get here and setClusters worked, clustering likely complete
381
+ // Check if there are actually clusters now
382
+ const { data: profiles } = await supabase
383
+ .from('profiles')
384
+ .select('cluster_label')
385
+ .not('cluster_label', 'is', null)
386
+ .limit(1);
387
+
388
+ if (profiles && profiles.length > 0) {
389
+ clearInterval(pollInterval);
390
+ setIsClusteringRunning(false);
391
+ alert('✅ Clustering complete! Clusters loaded.');
392
+ }
393
+ } catch (err) {
394
+ console.log('Still clustering...');
395
+ }
396
+ }, 5000);
397
+
398
+ // Clear interval after 5 minutes
399
+ setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000);
400
+
401
+ } catch (err) {
402
+ console.error('Error starting clustering:', err);
403
+ alert('Failed to start clustering. Check the backend logs.');
404
+ setIsClusteringRunning(false);
405
+ }
406
+ };
407
+
408
  const clusterEntries = Object.entries(clusters).sort((a, b) => b[1].length - a[1].length);
409
  const totalProfiles = Object.values(clusters).reduce((s, arr) => s + arr.length, 0);
410
 
 
467
  />
468
  </div>
469
  <motion.button
470
+ onClick={runClustering}
471
+ disabled={isClusteringRunning}
472
+ whileHover={!isClusteringRunning ? { scale: 1.04 } : {}}
473
+ whileTap={!isClusteringRunning ? { scale: 0.96 } : {}}
474
  style={{
475
+ backgroundColor: isClusteringRunning ? 'rgba(139,92,246,0.15)' : 'rgba(139,92,246,0.15)',
476
+ border: '1px solid rgba(139,92,246,0.4)',
477
+ color: isClusteringRunning ? '#94a3b8' : '#8B5CF6',
478
+ padding: '0.75rem 1.25rem',
479
+ borderRadius: '0.5rem',
480
+ cursor: isClusteringRunning ? 'not-allowed' : 'pointer',
481
+ fontWeight: '600',
482
+ fontSize: '0.85rem',
483
+ whiteSpace: 'nowrap',
484
+ opacity: isClusteringRunning ? 0.6 : 1,
485
+ transition: 'all 0.2s ease'
486
  }}
487
  >
488
+ {isClusteringRunning ? '⟳ Clustering...' : '⚡ Run Clustering'}
489
  </motion.button>
490
  </div>
491
 
src/components/Adminfront/TopPerformers.jsx CHANGED
@@ -37,6 +37,7 @@ export default function TopPerformers() {
37
  avatar_url,
38
  technical_skills,
39
  projects,
 
40
  summary
41
  ),
42
  jobs ( title )
@@ -62,13 +63,20 @@ export default function TopPerformers() {
62
  const profile = candidate.profiles || {};
63
  const job = candidate.jobs || {};
64
 
65
- // 1. EXPERIENCE (From Application Table)
66
- const expVal = parseFloat(candidate.experience) || 0;
67
  const expScore = Math.min(expVal * 10, 100);
68
 
69
- // 2. SKILLS (Numeric Score from DB Application)
70
- const dbSkillScore = candidate.match_score || 0;
71
- const skillScore = Math.min(dbSkillScore, 100);
 
 
 
 
 
 
 
72
 
73
  // Display Logic Only
74
  const skillsText = profile.technical_skills || "";
@@ -81,14 +89,15 @@ export default function TopPerformers() {
81
  const projCount = Array.isArray(projectsData) ? projectsData.length : 0;
82
  const projScore = Math.min(projCount * 20, 100);
83
 
84
- // 4. WEIGHTED FORMULA
85
- const totalWeight = config.skillsWeight + config.experienceWeight + config.projectWeight || 1;
86
-
87
- const rawScore = (
88
- (skillScore * config.skillsWeight) +
89
- (expScore * config.experienceWeight) +
90
- (projScore * config.projectWeight)
91
- ) / totalWeight;
 
92
 
93
  return {
94
  ...candidate,
 
37
  avatar_url,
38
  technical_skills,
39
  projects,
40
+ experience_years,
41
  summary
42
  ),
43
  jobs ( title )
 
63
  const profile = candidate.profiles || {};
64
  const job = candidate.jobs || {};
65
 
66
+ // 1. EXPERIENCE - Try multiple sources
67
+ const expVal = parseFloat(candidate.experience) || parseFloat(profile.experience_years) || 0;
68
  const expScore = Math.min(expVal * 10, 100);
69
 
70
+ // 2. SKILLS - Use match_score with fallback to skill count
71
+ let skillScore = parseFloat(candidate.match_score) || 0;
72
+ if (skillScore === 0) {
73
+ // Fallback: count technical skills
74
+ const skillsText = profile.technical_skills || "";
75
+ const skillCount = typeof skillsText === 'string' && skillsText.trim().length > 0
76
+ ? skillsText.split(',').length
77
+ : 0;
78
+ skillScore = Math.min(skillCount * 10, 100); // Each skill = 10 points
79
+ }
80
 
81
  // Display Logic Only
82
  const skillsText = profile.technical_skills || "";
 
89
  const projCount = Array.isArray(projectsData) ? projectsData.length : 0;
90
  const projScore = Math.min(projCount * 20, 100);
91
 
92
+ // 4. WEIGHTED FORMULA - BOOST MODEL
93
+ // FIXED: Each weight acts as a boost multiplier
94
+ // Weight of 5 = 1x, weight of 10 = 2x amplification
95
+ // This ensures increasing a weight ALWAYS increases the score
96
+ const skillBoost = skillScore * (1 + config.skillsWeight / 10);
97
+ const expBoost = expScore * (1 + config.experienceWeight / 10);
98
+ const projBoost = projScore * (1 + config.projectWeight / 10);
99
+
100
+ const rawScore = Math.min((skillBoost + expBoost + projBoost) / 3, 100);
101
 
102
  return {
103
  ...candidate,