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 || '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 && (
)}
);
};
// ─── 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.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 }) => (
))}
{/* 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 */}
{/* 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 ? (
) : 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)} />
)}
);
}