import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { supabase } from '../../supabaseClient'; import FullProfileOverlay from '../FullProfileOverlay'; // ─── Icons ─────────────────────────────────────────────────────────────────── const ClusterIcon = () => ( ); const UsersIcon = () => ( ); const SearchIcon = () => ( ); const ChevronDown = ({ open }) => ( ); const XIcon = () => ( ); // ─── Cluster colour palette ─────────────────────────────────────────────────── const CLUSTER_COLORS = [ { accent: '#EF4444', glow: 'rgba(239,68,68,0.15)', border: 'rgba(239,68,68,0.3)' }, { accent: '#8B5CF6', glow: 'rgba(139,92,246,0.15)', border: 'rgba(139,92,246,0.3)' }, { accent: '#06B6D4', glow: 'rgba(6,182,212,0.15)', border: 'rgba(6,182,212,0.3)' }, { accent: '#10B981', glow: 'rgba(16,185,129,0.15)', border: 'rgba(16,185,129,0.3)' }, { accent: '#F59E0B', glow: 'rgba(245,158,11,0.15)', border: 'rgba(245,158,11,0.3)' }, { accent: '#EC4899', glow: 'rgba(236,72,153,0.15)', border: 'rgba(236,72,153,0.3)' }, ]; const getColor = (idx) => CLUSTER_COLORS[idx % CLUSTER_COLORS.length]; // ─── Profile Card ───────────────────────────────────────────────────────────── const ProfileCard = ({ profile, accent, onView }) => { const [hovered, setHovered] = useState(false); const skills = Array.isArray(profile.technical_skills) ? profile.technical_skills.slice(0, 4) : typeof profile.technical_skills === 'string' ? profile.technical_skills.split(',').slice(0, 4).map(s => s.trim()) : []; return ( setHovered(true)} onMouseLeave={() => setHovered(false)} whileHover={{ y: -4, scale: 1.01 }} onClick={() => onView(profile)} style={{ backgroundColor: hovered ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.03)', border: `1px solid ${hovered ? accent : 'rgba(255,255,255,0.08)'}`, borderRadius: '12px', padding: '1rem', cursor: 'pointer', transition: 'border-color 0.2s', boxShadow: hovered ? `0 4px 20px ${accent}30` : 'none', }} >
{profile.full_name}

{profile.full_name || 'Unknown'}

{profile.headline || profile.role || '—'}

{profile.experience_years ? `${profile.experience_years} yrs exp` : 'No experience listed'}

{skills.length > 0 && (
{skills.map((s, i) => ( {s} ))}
)}
); }; // ─── Cluster Card ───────────────────────────────────────────────────────────── const ClusterCard = ({ label, profiles, colorIdx, searchQuery, onViewProfile }) => { const [expanded, setExpanded] = useState(true); const color = getColor(colorIdx); const filtered = profiles.filter(p => { const q = searchQuery.toLowerCase(); return ( (p.full_name || '').toLowerCase().includes(q) || (p.headline || '').toLowerCase().includes(q) || (p.role || '').toLowerCase().includes(q) ); }); if (searchQuery && filtered.length === 0) return null; return ( {/* Header */} {/* Body */} {expanded && (
{filtered.map(p => ( ))}
)}
); }; // ─── Profile Detail Modal ───────────────────────────────────────────────────── const ProfileModal = ({ profile, onClose }) => { if (!profile) return null; const skills = Array.isArray(profile.technical_skills) ? profile.technical_skills : typeof profile.technical_skills === 'string' ? profile.technical_skills.split(',').map(s => s.trim()) : []; return ( e.stopPropagation()} style={{ backgroundColor: '#0f172a', backgroundImage: ` radial-gradient(at 0% 0%, rgba(139,92,246,0.2) 0px, transparent 50%), radial-gradient(at 100% 100%, rgba(239,68,68,0.2) 0px, transparent 50%) `, border: '1px solid rgba(255,255,255,0.1)', borderRadius: '20px', width: '100%', maxWidth: '540px', maxHeight: '80vh', overflowY: 'auto', boxShadow: '0 25px 50px rgba(0,0,0,0.5)', padding: '2rem', }} > {/* Close */}
{profile.full_name}

{profile.full_name}

{profile.headline || profile.role || '—'}

{/* Stats Row */}
{[ { label: 'Experience', value: profile.experience_years ? `${profile.experience_years} yrs` : '—' }, { label: 'Cluster', value: profile.cluster_label || '—' }, { label: 'Email', value: profile.email ? profile.email.split('@')[0] : '—' }, ].map(({ label, value }) => (

{label}

{value}

))}
{/* Summary */} {profile.summary && (

Summary

{profile.summary}

)} {/* Skills */} {skills.length > 0 && (

Technical Skills

{skills.map((s, i) => ( {s} ))}
)} {/* Education */} {profile.education && (

Education

{typeof profile.education === 'string' ? profile.education : JSON.stringify(profile.education)}

)}
); }; // ─── MAIN PAGE ──────────────────────────────────────────────────────────────── export default function TalentClusters() { const [clusters, setClusters] = useState({}); // { labelName: [profiles] } const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedProfile, setSelectedProfile] = useState(null); const [error, setError] = useState(null); const [isClusteringRunning, setIsClusteringRunning] = useState(false); useEffect(() => { fetchClusters(); }, []); const fetchClusters = async () => { setIsLoading(true); setError(null); try { const { data, error } = await supabase .from('profiles') .select('id, full_name, email, avatar_url, headline, role, experience_years, technical_skills, summary, education, cluster_label') .not('cluster_label', 'is', null); if (error) throw error; // Group by cluster_label const grouped = {}; data.forEach(profile => { const label = profile.cluster_label || 'Uncategorized'; if (!grouped[label]) grouped[label] = []; grouped[label].push(profile); }); setClusters(grouped); } catch (err) { console.error('Failed to fetch clusters:', err); setError('Failed to load talent clusters. Please try again.'); } finally { setIsLoading(false); } }; const runClustering = async () => { setIsClusteringRunning(true); try { const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const response = await fetch(`${API_URL}/run-clustering`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('Failed to start clustering'); } const data = await response.json(); console.log('✅ Clustering started:', data); alert('Clustering pipeline started! This may take a few minutes. You will be notified when complete.'); // Poll for completion (check every 5 seconds for up to 5 minutes) const pollInterval = setInterval(async () => { try { await fetchClusters(); // If we get here and setClusters worked, clustering likely complete // Check if there are actually clusters now const { data: profiles } = await supabase .from('profiles') .select('cluster_label') .not('cluster_label', 'is', null) .limit(1); if (profiles && profiles.length > 0) { clearInterval(pollInterval); setIsClusteringRunning(false); alert('✅ Clustering complete! Clusters loaded.'); } } catch (err) { console.log('Still clustering...'); } }, 5000); // Clear interval after 5 minutes setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000); } catch (err) { console.error('Error starting clustering:', err); alert('Failed to start clustering. Check the backend logs.'); setIsClusteringRunning(false); } }; const clusterEntries = Object.entries(clusters).sort((a, b) => b[1].length - a[1].length); const totalProfiles = Object.values(clusters).reduce((s, arr) => s + arr.length, 0); return (
{/* Header */}

Talent Clusters

AI-grouped candidate profiles based on skills and experience similarity.

{/* Stats Bar */}
{[ { label: 'Total Clusters', value: clusterEntries.length, color: '#EF4444' }, { label: 'Total Profiles', value: totalProfiles, color: '#8B5CF6' }, { label: 'Avg. Cluster Size', value: clusterEntries.length ? Math.round(totalProfiles / clusterEntries.length) : 0, color: '#06B6D4' }, ].map(({ label, value, color }) => (

{label}

{isLoading ? '—' : value}

))}
{/* Search + Refresh */}
setSearchQuery(e.target.value)} style={{ width: '100%', padding: '0.75rem 0.75rem 0.75rem 2.25rem', borderRadius: '0.5rem', border: '1px solid rgba(239,68,68,0.3)', backgroundColor: 'rgba(255,255,255,0.04)', color: 'white', fontSize: '0.9rem', outline: 'none', boxSizing: 'border-box' }} />
{isClusteringRunning ? '⟳ Clustering...' : '⚡ Run Clustering'}
{/* Content */} {isLoading ? (

Loading talent clusters…

) : error ? (

{error}

) : clusterEntries.length === 0 ? (

No clusters found. Run the clustering pipeline first.

) : ( <> {/* Cluster grid legend */}
{clusterEntries.map(([label, profiles], idx) => { const color = getColor(idx); return ( {label} ({profiles.length}) ); })}
{/* Cluster cards */} {clusterEntries.map(([label, profiles], idx) => ( ))} )} {/* Profile modal */} {selectedProfile && ( setSelectedProfile(null)} /> )}
); }