Spaces:
Sleeping
Sleeping
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
|
| 5 |
import ScheduleInterviewModal from '../ScheduleInterviewModal';
|
| 6 |
|
| 7 |
// --- ICONS ---
|
|
@@ -454,10 +454,9 @@ export default function AdminInterviewManagement() {
|
|
| 454 |
|
| 455 |
<AnimatePresence>
|
| 456 |
{isDrawerOpen && (
|
| 457 |
-
<
|
| 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={
|
| 422 |
-
|
| 423 |
-
|
|
|
|
| 424 |
style={{
|
| 425 |
-
backgroundColor: 'rgba(
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
}}
|
| 429 |
>
|
| 430 |
-
|
| 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
|
| 66 |
-
const expVal = parseFloat(candidate.experience) || 0;
|
| 67 |
const expScore = Math.min(expVal * 10, 100);
|
| 68 |
|
| 69 |
-
// 2. SKILLS
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
| 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,
|