import React, { useState, useEffect } from 'react'; import { Box, Typography, Paper, Accordion, AccordionSummary, AccordionDetails, CircularProgress, Grid, Chip, Divider, Card, CardContent, useTheme, LinearProgress } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import ErrorIcon from '@mui/icons-material/Error'; import PendingIcon from '@mui/icons-material/Pending'; import WarningIcon from '@mui/icons-material/Warning'; import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession, JobRequirements, SkillMatch } from 'types/types'; import { useAuth } from 'hooks/AuthContext'; import { BackstoryPageProps } from './BackstoryTab'; import { toCamelCase } from 'types/conversion'; interface Job { title: string; description: string; } interface JobAnalysisProps extends BackstoryPageProps { job: Job; candidate: Candidate; } const defaultMessage: ChatMessageUser = { type: "preparing", status: "done", sender: "user", sessionId: "", timestamp: new Date(), content: "" }; const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) => { const { job, candidate, setSnack, } = props const { apiClient } = useAuth(); const theme = useTheme(); const [jobRequirements, setJobRequirements] = useState(null); const [requirements, setRequirements] = useState([]); const [skillMatches, setSkillMatches] = useState([]); const [creatingSession, setCreatingSession] = useState(false); const [loadingRequirements, setLoadingRequirements] = useState(false); const [expanded, setExpanded] = useState(false); const [overallScore, setOverallScore] = useState(0); const [requirementsSession, setRequirementsSession] = useState(null); const [statusMessage, setStatusMessage] = useState(null); // Handle accordion expansion const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { setExpanded(isExpanded ? panel : false); }; useEffect(() => { if (requirementsSession || creatingSession) { return; } const createSession = async () => { try { const session: ChatSession = await apiClient.createCandidateChatSession( candidate.username, 'job_requirements', `Generate requirements for ${job.title}` ); setSnack("Job analysis session started"); setRequirementsSession(session); } catch (error) { console.log(error); setSnack("Unable to create requirements session", "error"); } setCreatingSession(false); }; setCreatingSession(true); createSession(); }, [requirementsSession, apiClient, candidate]); // Fetch initial requirements useEffect(() => { if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) { return; } const getRequirements = async () => { setLoadingRequirements(true); try { const chatMessage: ChatMessageUser = { ...defaultMessage, sessionId: requirementsSession.id || '', content: job.description }; apiClient.sendMessageStream(chatMessage, { onMessage: (msg: ChatMessage) => { console.log(`onMessage: ${msg.type}`, msg); if (msg.type === "response") { const incoming: any = toCamelCase(JSON.parse(msg.content || '')); const requirements: string[] = ['technicalSkills', 'experienceRequirements'].flatMap((type) => { return ['required', 'preferred'].flatMap((level) => { return incoming[type][level].map((s: string) => s); }) }); ['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(l => { if (incoming[l]) { incoming[l].forEach((s: string) => requirements.push(s)); } }); // Initialize skill matches with pending status const initialSkillMatches = requirements.map(req => ({ requirement: req, status: 'pending' as const, matchScore: 0, assessment: '', citations: [] })); setRequirements(requirements); setSkillMatches(initialSkillMatches); setStatusMessage(null); setLoadingRequirements(false); } }, onError: (error: string | ChatMessageBase) => { console.log("onError:", error); // Type-guard to determine if this is a ChatMessageBase or a string if (typeof error === "object" && error !== null && "content" in error) { setSnack(error.content || 'Error obtaining requirements from job description.', "error"); } else { setSnack(error as string, "error"); } setLoadingRequirements(false); }, onStreaming: (chunk: ChatMessageBase) => { // console.log("onStreaming:", chunk); }, onStatusChange: (status: string) => { console.log(`onStatusChange: ${status}`); }, onComplete: () => { console.log("onComplete"); setStatusMessage(null); setLoadingRequirements(false); } }); } catch (error) { console.error('Failed to send message:', error); setLoadingRequirements(false); } }; getRequirements(); }, [job, requirementsSession]); // Fetch match data for each requirement useEffect(() => { const fetchMatchData = async () => { if (requirements.length === 0) return; // Process requirements one by one for (let i = 0; i < requirements.length; i++) { try { const match: SkillMatch = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i]); console.log(match); setSkillMatches(prev => { const updated = [...prev]; updated[i] = match; return updated; }); // Update overall score setSkillMatches(current => { const completedMatches = current.filter(match => match.status === 'complete'); if (completedMatches.length > 0) { const newOverallScore = completedMatches.reduce((sum, match) => sum + match.matchScore, 0) / completedMatches.length; setOverallScore(newOverallScore); } return current; }); } catch (error) { console.error(`Error fetching match for requirement ${requirements[i]}:`, error); setSkillMatches(prev => { const updated = [...prev]; updated[i] = { ...updated[i], status: 'error', assessment: 'Failed to analyze this requirement.' }; return updated; }); } } }; if (!loadingRequirements) { fetchMatchData(); } }, [requirements, loadingRequirements]); // Get color based on match score const getMatchColor = (score: number): string => { if (score >= 80) return theme.palette.success.main; if (score >= 60) return theme.palette.info.main; if (score >= 40) return theme.palette.warning.main; return theme.palette.error.main; }; // Get icon based on status const getStatusIcon = (status: string, score: number) => { if (status === 'pending') return ; if (status === 'error') return ; if (score >= 70) return ; if (score >= 40) return ; return ; }; return ( Job Match Analysis Job: {job.title} Candidate: {candidate.fullName} Overall Match: {`${Math.round(overallScore)}%`} = 80 ? "Excellent Match" : overallScore >= 60 ? "Good Match" : overallScore >= 40 ? "Partial Match" : "Low Match" } sx={{ bgcolor: getMatchColor(overallScore), color: 'white', fontWeight: 'bold' }} /> {loadingRequirements ? ( Analyzing job requirements... ) : ( Requirements Analysis {skillMatches.map((match, index) => ( } aria-controls={`panel${index}bh-content`} id={`panel${index}bh-header`} sx={{ bgcolor: match.status === 'complete' ? `${getMatchColor(match.matchScore)}22` // Add transparency : 'inherit' }} > {getStatusIcon(match.status, match.matchScore)} {match.requirement} {match.status === 'complete' ? ( ) : match.status === 'pending' ? ( ) : ( )} {match.status === 'pending' ? ( Analyzing candidate's match for this requirement... ) : match.status === 'error' ? ( {match.assessment || "An error occurred while analyzing this requirement."} ) : ( Assessment: {match.assessment} Supporting Evidence: {match.citations && match.citations.length > 0 ? ( match.citations.map((citation, citIndex) => ( "{citation.text}" Source: {citation.source} )) ) : ( No specific evidence found in candidate's profile. )} )} ))} )} ); }; export { JobMatchAnalysis };