import React, { useState, useEffect } from 'react'; import { Box, Typography, Paper, Accordion, AccordionSummary, AccordionDetails, CircularProgress, Grid, Chip, Divider, Card, CardContent, useTheme, LinearProgress, useMediaQuery, Button, } 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, ChatMessageError, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, ChatSession, EvidenceDetail, JobRequirements, SkillAssessment, SkillStatus, } from 'types/types'; import { useAuth } from 'hooks/AuthContext'; import { BackstoryPageProps } from './BackstoryTab'; import { Job } from 'types/types'; import { StyledMarkdown } from './StyledMarkdown'; import { Scrollable } from './Scrollable'; import { useAppState } from 'hooks/GlobalContext'; import * as Types from 'types/types'; import JsonView from '@uiw/react-json-view'; import { VectorVisualizer } from './VectorVisualizer'; import { JobInfo } from './ui/JobInfo'; interface JobAnalysisProps extends BackstoryPageProps { job: Job; candidate: Candidate; variant?: 'small' | 'normal'; onAnalysisComplete: (skills: SkillAssessment[]) => void; } const defaultMessage: ChatMessage = { status: 'done', type: 'text', sessionId: '', timestamp: new Date(), content: '', role: 'assistant', metadata: null as any, }; interface SkillMatch extends SkillAssessment { domain: string; status: SkillStatus; matchScore: number; } const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) => { const { job, candidate, onAnalysisComplete, variant = 'normal' } = props; const { apiClient } = useAuth(); const { setSnack } = useAppState(); const theme = useTheme(); const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]); 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); const [startAnalysis, setStartAnalysis] = useState(false); const [analyzing, setAnalyzing] = useState(false); const [matchStatus, setMatchStatus] = useState(''); const [matchStatusType, setMatchStatusType] = useState(null); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); // Handle accordion expansion const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { setExpanded(isExpanded ? panel : false); }; const initializeRequirements = (job: Job) => { if (!job || !job.requirements) { return; } const requirements: { requirement: string; domain: string }[] = []; if (job.requirements?.technicalSkills) { job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)', }) ); job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)', }) ); } if (job.requirements?.experienceRequirements) { job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }) ); job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)', }) ); } if (job.requirements?.softSkills) { job.requirements.softSkills.forEach(req => requirements.push({ requirement: req, domain: 'Soft Skills' }) ); } if (job.requirements?.experience) { job.requirements.experience.forEach(req => requirements.push({ requirement: req, domain: 'Experience' }) ); } if (job.requirements?.education) { job.requirements.education.forEach(req => requirements.push({ requirement: req, domain: 'Education' }) ); } if (job.requirements?.certifications) { job.requirements.certifications.forEach(req => requirements.push({ requirement: req, domain: 'Certifications' }) ); } if (job.requirements?.preferredAttributes) { job.requirements.preferredAttributes.forEach(req => requirements.push({ requirement: req, domain: 'Preferred Attributes' }) ); } const initialSkillMatches: SkillMatch[] = requirements.map(req => ({ skill: req.requirement, skillModified: req.requirement, candidateId: candidate.id || '', domain: req.domain, status: 'waiting' as const, assessment: '', description: '', evidenceFound: false, evidenceStrength: 'none', evidenceDetails: [], matchScore: 0, })); setRequirements(requirements); setSkillMatches(initialSkillMatches); setStatusMessage(null); setLoadingRequirements(false); setOverallScore(0); }; useEffect(() => { initializeRequirements(job); }, [job]); const skillMatchHandlers = { onStatus: (status: Types.ChatMessageStatus) => { setMatchStatusType(status.activity); setMatchStatus(status.content.toLowerCase()); }, }; // Fetch match data for each requirement useEffect(() => { if (!startAnalysis || analyzing || !job.requirements) { return; } const fetchMatchData = async (skills: SkillAssessment[]) => { if (requirements.length === 0) return; // Process requirements one by one for (let i = 0; i < requirements.length; i++) { try { setSkillMatches(prev => { const updated = [...prev]; updated[i] = { ...updated[i], status: 'pending' }; return updated; }); const request: any = await apiClient.candidateMatchForRequirement( candidate.id || '', requirements[i].requirement, skillMatchHandlers ); const result = await request.promise; const skillMatch = result.skillAssessment; skills.push(skillMatch); setMatchStatus(''); let matchScore = 0; switch (skillMatch.evidenceStrength.toUpperCase()) { case 'STRONG': matchScore = 100; break; case 'MODERATE': matchScore = 75; break; case 'WEAK': matchScore = 50; break; case 'NONE': matchScore = 0; break; } if ( skillMatch.evidenceStrength == 'NONE' && skillMatch.citations && skillMatch.citations.length > 3 ) { matchScore = Math.min(skillMatch.citations.length * 8, 40); } const match: SkillMatch = { ...skillMatch, status: 'complete', matchScore, domain: requirements[i].domain, }; 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; }); } } }; setAnalyzing(true); const skills: SkillAssessment[] = []; fetchMatchData(skills).then(() => { setAnalyzing(false); setStartAnalysis(false); onAnalysisComplete && onAnalysisComplete(skills); }); }, [job, onAnalysisComplete, startAnalysis, analyzing, 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' || status === 'waiting') return ; if (status === 'error') return ; if (score >= 70) return ; if (score >= 40) return ; return ; }; const beginAnalysis = () => { initializeRequirements(job); setStartAnalysis(true); }; return ( {variant !== 'small' && } {overallScore !== 0 && ( <> 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.skill} {match.domain} {match.status === 'complete' ? ( ) : match.status === 'waiting' ? ( ) : match.status === 'pending' ? ( ) : ( )} {match.status === 'pending' ? ( Analyzing candidate's match for this requirement... {matchStatus} ) : match.status === 'error' ? ( {match.assessment || 'An error occurred while analyzing this requirement.'} ) : ( Assessment {match.assessment} Supporting Evidence {match.evidenceDetails && match.evidenceDetails.length > 0 ? ( match.evidenceDetails.map((evidence, evndex) => ( "{evidence.quote}" Relevance: {evidence.context} Source: {evidence.source} {/* */} )) ) : ( No specific evidence found in candidate's profile. )} Skill description {match.description} {/* { match.ragResults && match.ragResults.length !== 0 && <> RAG Information } */} )} ))} )} ); }; export { JobMatchAnalysis };