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, JobRequirements, SkillMatch } from 'types/types'; import { useAuth } from 'hooks/AuthContext'; import { BackstoryPageProps } from './BackstoryTab'; import { toCamelCase } from 'types/conversion'; import { Job } from 'types/types'; import { StyledMarkdown } from './StyledMarkdown'; import { Scrollable } from './Scrollable'; import { start } from 'repl'; import { TypesElement } from '@uiw/react-json-view'; interface JobAnalysisProps extends BackstoryPageProps { job: Job; candidate: Candidate; } const defaultMessage: ChatMessage = { status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant" }; const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) => { const { job, candidate, setSnack, submitQuery } = props const { apiClient } = useAuth(); const backstoryProps = { setSnack, submitQuery }; 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 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 = requirements.map(req => ({ requirement: req.requirement, domain: req.domain, status: 'waiting' as const, matchScore: 0, assessment: '', description: '', citations: [] })); setRequirements(requirements); setSkillMatches(initialSkillMatches); setStatusMessage(null); setLoadingRequirements(false); setOverallScore(0); } useEffect(() => { initializeRequirements(job); }, [job]); // Fetch match data for each requirement useEffect(() => { if (!startAnalysis || analyzing || !job.requirements) { return; } const fetchMatchData = async () => { 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 result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement); const skillMatch = result.skillMatch; let matchScore: number = 0; switch (skillMatch.evidenceStrength) { 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 = { status: "complete", matchScore, domain: requirements[i].domain, requirement: skillMatch.skill, assessment: skillMatch.assessment, citations: skillMatch.evidenceDetails.map((evidence: any) => { return { source: evidence.source, text: evidence.quote, context: evidence.context } }), description: skillMatch.description }; 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); fetchMatchData().then(() => { setAnalyzing(false); setStartAnalysis(false) }); }, [job, 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 ( Company: {job.company || "N/A"} Job Title: {job.title || "N/A"} Backstory Generated Job Summary: {job.summary || "N/A"} Job ID: {job.id} Original Job Description: {} {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.requirement} {match.domain} {match.status === 'complete' ? ( ) : match.status === 'waiting' ? ( ) : 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}" Relevance: {citation.context} Source: {citation.source} {/* */} )) ) : ( No specific evidence found in candidate's profile. )} Skill description {match.description} )} ))} )} ); }; export { JobMatchAnalysis };