823 lines
26 KiB
TypeScript
823 lines
26 KiB
TypeScript
import React, { useState, useEffect, useCallback, JSX, useMemo } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
CircularProgress,
|
|
Chip,
|
|
Card,
|
|
CardContent,
|
|
useTheme,
|
|
LinearProgress,
|
|
useMediaQuery,
|
|
Button,
|
|
Paper,
|
|
SxProps,
|
|
Tooltip,
|
|
IconButton,
|
|
} 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 ModelTrainingIcon from '@mui/icons-material/ModelTraining';
|
|
import PendingIcon from '@mui/icons-material/Pending';
|
|
import WarningIcon from '@mui/icons-material/Warning';
|
|
import { Candidate, SkillAssessment, SkillStatus } from 'types/types';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { BackstoryPageProps } from './BackstoryTab';
|
|
import { Job } from 'types/types';
|
|
import * as Types from 'types/types';
|
|
import { JobInfo } from './ui/JobInfo';
|
|
import { Scrollable } from 'components/Scrollable';
|
|
|
|
interface JobAnalysisScore {
|
|
score: number;
|
|
skills: SkillAssessment[];
|
|
}
|
|
|
|
interface JobAnalysisProps extends BackstoryPageProps {
|
|
job: Job;
|
|
candidate: Candidate;
|
|
variant?: 'small' | 'normal';
|
|
onAnalysisComplete: (analysis: JobAnalysisScore) => void;
|
|
}
|
|
|
|
interface SkillMatch extends SkillAssessment {
|
|
domain: string;
|
|
status: SkillStatus;
|
|
matchScore: number;
|
|
}
|
|
|
|
const JobMatchScore: React.FC<{ score: number; variant?: 'small' | 'normal'; sx?: SxProps }> = ({
|
|
variant = 'normal',
|
|
score,
|
|
sx = {},
|
|
}) => {
|
|
const theme = useTheme();
|
|
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;
|
|
};
|
|
const suffix = variant === 'small' ? '' : ' Match';
|
|
|
|
return (
|
|
<Box
|
|
className="JobMatchScore"
|
|
sx={{
|
|
justifyContent: 'center',
|
|
m: 0,
|
|
p: 0,
|
|
width: variant === 'small' ? '8rem' : '10rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
flexDirection: 'row',
|
|
...sx,
|
|
}}
|
|
>
|
|
<Chip
|
|
label={
|
|
score >= 80
|
|
? `Excellent${suffix}`
|
|
: score >= 60
|
|
? `Good${suffix}`
|
|
: score >= 40
|
|
? `Partial${suffix}`
|
|
: `Low${suffix}`
|
|
}
|
|
sx={{
|
|
bgcolor: getMatchColor(score),
|
|
color: 'white',
|
|
fontWeight: 'bold',
|
|
fontSize: variant === 'small' ? '0.7rem' : '1rem',
|
|
}}
|
|
/>
|
|
{variant !== 'small' && (
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
display: 'inline-flex',
|
|
p: 0,
|
|
m: 0,
|
|
}}
|
|
>
|
|
<CircularProgress
|
|
variant="determinate"
|
|
value={score}
|
|
size={60}
|
|
thickness={5}
|
|
sx={{
|
|
color: getMatchColor(score),
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
top: 0,
|
|
left: 0,
|
|
bottom: 0,
|
|
right: 0,
|
|
position: 'absolute',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="caption"
|
|
component="div"
|
|
sx={{ fontWeight: 'bold', fontSize: '1rem' }}
|
|
>
|
|
{`${Math.round(score)}%`}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const calculateScore = (skillMatch: SkillAssessment): number => {
|
|
let score = 0;
|
|
switch (skillMatch.evidenceStrength.toUpperCase()) {
|
|
case 'STRONG':
|
|
score = 100;
|
|
break;
|
|
case 'MODERATE':
|
|
score = 75;
|
|
break;
|
|
case 'WEAK':
|
|
score = 50;
|
|
break;
|
|
case 'NONE':
|
|
score = 0;
|
|
break;
|
|
}
|
|
if (
|
|
skillMatch.evidenceStrength === 'none' &&
|
|
skillMatch.evidenceDetails &&
|
|
skillMatch.evidenceDetails.length > 3
|
|
) {
|
|
score = Math.min(skillMatch.evidenceDetails.length * 8, 40);
|
|
}
|
|
return score;
|
|
};
|
|
|
|
const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => {
|
|
const { job, candidate, onAnalysisComplete, variant = 'normal' } = props;
|
|
const { apiClient } = useAuth();
|
|
const theme = useTheme();
|
|
const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]);
|
|
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
|
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
|
|
const [expanded, setExpanded] = useState<string | false>(false);
|
|
const [overallScore, setOverallScore] = useState<number>(0);
|
|
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
|
const [matchStatus, setMatchStatus] = useState<string>('');
|
|
const [percentage, setPercentage] = useState<number>(0);
|
|
const [analysis, setAnalysis] = useState<JobAnalysisScore | null>(null);
|
|
const [startAnalysis, setStartAnalysis] = useState<boolean>(true);
|
|
const [firstRun, setFirstRun] = useState<boolean>(true);
|
|
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 = useCallback(
|
|
(job: Job): void => {
|
|
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);
|
|
setLoadingRequirements(false);
|
|
setOverallScore(0);
|
|
},
|
|
[candidate.id]
|
|
);
|
|
|
|
useEffect(() => {
|
|
initializeRequirements(job);
|
|
}, [job, initializeRequirements]);
|
|
|
|
const skillMatchHandlers = useMemo(() => {
|
|
return {
|
|
onStatus: (status: Types.ChatMessageStatus): void => {
|
|
console.log('Skill Match Status:', status.content);
|
|
setMatchStatus(status.content);
|
|
},
|
|
};
|
|
}, [setMatchStatus]);
|
|
|
|
// Fetch match data for each requirement
|
|
useEffect(() => {
|
|
if (
|
|
(!startAnalysis && !firstRun) ||
|
|
analyzing ||
|
|
!job.requirements ||
|
|
requirements.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const fetchMatchData = async (firstRun: boolean): Promise<void> => {
|
|
const currentAnalysis = await apiClient.getJobAnalysis(job, candidate);
|
|
for (let i = 0; i < requirements.length; i++) {
|
|
try {
|
|
let match: SkillMatch;
|
|
const existingMatch = currentAnalysis?.skills.find(
|
|
(match: SkillAssessment) => match.skill === requirements[i].requirement
|
|
);
|
|
if (existingMatch) {
|
|
match = {
|
|
...existingMatch,
|
|
status: 'complete',
|
|
matchScore: calculateScore(existingMatch),
|
|
domain: requirements[i].domain,
|
|
};
|
|
} else {
|
|
if (firstRun) {
|
|
continue;
|
|
}
|
|
setSkillMatches(prev => {
|
|
const updated = [...prev];
|
|
updated[i] = { ...updated[i], status: 'pending' };
|
|
return updated;
|
|
});
|
|
|
|
const request = await apiClient.candidateMatchForRequirement(
|
|
candidate.id || '',
|
|
requirements[i].requirement,
|
|
false,
|
|
skillMatchHandlers
|
|
);
|
|
const result = await request.promise; /* Wait for the streaming result to complete */
|
|
const skillMatch = result.skillAssessment;
|
|
setMatchStatus('');
|
|
|
|
match = {
|
|
...skillMatch,
|
|
status: 'complete',
|
|
matchScore: calculateScore(skillMatch),
|
|
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(Math.round(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;
|
|
});
|
|
}
|
|
setPercentage(Math.round(((i + 1) / requirements.length) * 100));
|
|
}
|
|
};
|
|
|
|
setAnalyzing(true);
|
|
setPercentage(0);
|
|
|
|
fetchMatchData(firstRun).then(() => {
|
|
setAnalyzing(false);
|
|
setStartAnalysis(false);
|
|
});
|
|
setFirstRun(false);
|
|
}, [
|
|
job,
|
|
startAnalysis,
|
|
analyzing,
|
|
requirements,
|
|
loadingRequirements,
|
|
apiClient,
|
|
candidate.id,
|
|
skillMatchHandlers,
|
|
firstRun,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (skillMatches.length === 0) {
|
|
return;
|
|
}
|
|
const finishedAnalysis = skillMatches.every(
|
|
match => match.status === 'complete' || match.status === 'error'
|
|
);
|
|
if (!finishedAnalysis) {
|
|
return;
|
|
}
|
|
if (analysis && analysis.score === overallScore) {
|
|
return; // No change in score, skip setting analysis
|
|
}
|
|
const newAnalysis: JobAnalysisScore = {
|
|
score: overallScore,
|
|
skills: skillMatches,
|
|
};
|
|
setAnalysis(newAnalysis);
|
|
onAnalysisComplete && onAnalysisComplete(newAnalysis);
|
|
}, [onAnalysisComplete, skillMatches, overallScore, analysis]);
|
|
|
|
// 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): JSX.Element => {
|
|
if (status === 'pending' || status === 'waiting') return <PendingIcon />;
|
|
if (status === 'error') return <ErrorIcon color="error" />;
|
|
if (score >= 70) return <CheckCircleIcon color="success" />;
|
|
if (score >= 40) return <WarningIcon color="warning" />;
|
|
return <ErrorIcon color="error" />;
|
|
};
|
|
|
|
const handleAssesmentRegenerate = async (index: number, skill: string): Promise<void> => {
|
|
setSkillMatches(prev => {
|
|
const updated = [...prev];
|
|
updated[index] = {
|
|
...updated[index],
|
|
status: 'pending',
|
|
assessment: '',
|
|
evidenceDetails: [],
|
|
evidenceStrength: 'none',
|
|
matchScore: 0,
|
|
};
|
|
return updated;
|
|
});
|
|
setMatchStatus('Regenerating assessment...');
|
|
const request = apiClient.candidateMatchForRequirement(
|
|
candidate.id || '',
|
|
skill,
|
|
true,
|
|
skillMatchHandlers
|
|
);
|
|
const result = await request.promise; /* Wait for the streaming result to complete */
|
|
const skillMatch = result.skillAssessment;
|
|
setMatchStatus('');
|
|
setSkillMatches(prev => {
|
|
const updated = [...prev];
|
|
updated[index] = {
|
|
...skillMatch,
|
|
status: 'complete',
|
|
matchScore: calculateScore(skillMatch),
|
|
domain: updated[index].domain,
|
|
};
|
|
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(Math.round(newOverallScore));
|
|
}
|
|
return current;
|
|
});
|
|
setAnalysis({
|
|
score: overallScore,
|
|
skills: skillMatches,
|
|
});
|
|
onAnalysisComplete &&
|
|
onAnalysisComplete({
|
|
score: overallScore,
|
|
skills: skillMatches,
|
|
});
|
|
setStartAnalysis(false);
|
|
setAnalyzing(false);
|
|
setFirstRun(false);
|
|
};
|
|
|
|
const beginAnalysis = (): void => {
|
|
initializeRequirements(job);
|
|
setStartAnalysis(true);
|
|
};
|
|
|
|
return (
|
|
<Scrollable
|
|
className="JobMatchAnalysis"
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
m: 0,
|
|
p: 1,
|
|
width: '100%',
|
|
minHeight: 0,
|
|
flexGrow: 1,
|
|
}}
|
|
>
|
|
{variant !== 'small' && <JobInfo job={job} variant="normal" />}
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
mb: isMobile ? 1 : 2,
|
|
gap: 1,
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: isMobile ? 'column' : 'row',
|
|
flexGrow: 1,
|
|
gap: 1,
|
|
}}
|
|
>
|
|
{analyzing && overallScore !== 0 && (
|
|
<JobMatchScore
|
|
score={overallScore}
|
|
sx={{ width: isMobile ? '100%' : 'auto', flexGrow: 1 }}
|
|
/>
|
|
)}
|
|
{analyzing && (
|
|
<Paper
|
|
sx={{
|
|
width: '10rem',
|
|
ml: 1,
|
|
p: 1,
|
|
gap: 1,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<Typography variant="h5" component="h2" sx={{ m: 0, fontSize: '1rem' }}>
|
|
Analyzing
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
position: 'relative',
|
|
display: 'inline-flex',
|
|
}}
|
|
>
|
|
<CircularProgress
|
|
variant="determinate"
|
|
value={percentage}
|
|
size={60}
|
|
thickness={5}
|
|
sx={{
|
|
color: 'orange',
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
top: 0,
|
|
left: 0,
|
|
bottom: 0,
|
|
right: 0,
|
|
position: 'absolute',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Typography variant="caption" component="div" sx={{ fontWeight: 'bold' }}>
|
|
{`${Math.round(percentage)}%`}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
<Button
|
|
sx={{ marginLeft: 'auto' }}
|
|
disabled={analyzing || startAnalysis}
|
|
onClick={beginAnalysis}
|
|
variant="contained"
|
|
>
|
|
{analyzing ? 'Assessment in Progress' : 'Analyze Waiting Skills'}
|
|
</Button>
|
|
</Box>
|
|
|
|
{loadingRequirements ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
|
<CircularProgress />
|
|
<Typography variant="h6" sx={{ ml: 2 }}>
|
|
Analyzing job requirements...
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<Typography variant="h5" component="h2" gutterBottom>
|
|
Requirements Analysis
|
|
</Typography>
|
|
|
|
{skillMatches.map((match, index) => (
|
|
<Accordion
|
|
key={index}
|
|
expanded={expanded === `panel${index}`}
|
|
onChange={handleAccordionChange(`panel${index}`)}
|
|
sx={{
|
|
mb: 2,
|
|
border: '1px solid',
|
|
borderColor:
|
|
match.status === 'complete'
|
|
? getMatchColor(match.matchScore)
|
|
: theme.palette.divider,
|
|
}}
|
|
>
|
|
<AccordionSummary
|
|
expandIcon={<ExpandMoreIcon />}
|
|
aria-controls={`panel${index}bh-content`}
|
|
id={`panel${index}bh-header`}
|
|
sx={{
|
|
bgcolor:
|
|
match.status === 'complete'
|
|
? `${getMatchColor(match.matchScore)}22` // Add transparency
|
|
: 'inherit',
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
{getStatusIcon(match.status, match.matchScore)}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 0,
|
|
p: 0,
|
|
m: 0,
|
|
}}
|
|
>
|
|
<Typography
|
|
sx={{
|
|
ml: 1,
|
|
mb: 0,
|
|
fontWeight: 'medium',
|
|
marginBottom: '0px !important',
|
|
}}
|
|
>
|
|
{match.skill}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}>
|
|
{match.domain}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
{match.status === 'complete' ? (
|
|
<Chip
|
|
label={`${match.matchScore}% Match`}
|
|
size="small"
|
|
sx={{
|
|
bgcolor: getMatchColor(match.matchScore),
|
|
color: 'white',
|
|
minWidth: 90,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
) : match.status === 'waiting' ? (
|
|
<Chip
|
|
label="Waiting..."
|
|
size="small"
|
|
sx={{
|
|
bgcolor: 'rgb(189, 173, 85)',
|
|
color: 'white',
|
|
minWidth: 90,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
) : match.status === 'pending' ? (
|
|
<Chip
|
|
label="Analyzing..."
|
|
size="small"
|
|
sx={{
|
|
bgcolor: theme.palette.grey[400],
|
|
color: 'white',
|
|
minWidth: 90,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
) : (
|
|
<Chip
|
|
label="Error"
|
|
size="small"
|
|
sx={{
|
|
bgcolor: theme.palette.error.main,
|
|
color: 'white',
|
|
minWidth: 90,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</AccordionSummary>
|
|
|
|
<AccordionDetails>
|
|
{match.status === 'pending' ? (
|
|
<Box sx={{ width: '100%', p: 2 }}>
|
|
<LinearProgress />
|
|
<Typography sx={{ mt: 2 }}>
|
|
Analyzing candidate's match for this requirement... {matchStatus}
|
|
</Typography>
|
|
</Box>
|
|
) : match.status === 'error' ? (
|
|
<Typography color="error">
|
|
{match.assessment || 'An error occurred while analyzing this requirement.'}
|
|
</Typography>
|
|
) : (
|
|
<Box>
|
|
<Typography variant="h6" gutterBottom>
|
|
Assessment
|
|
</Typography>
|
|
|
|
<Typography paragraph sx={{ mb: 3 }}>
|
|
{match.assessment}
|
|
</Typography>
|
|
|
|
<Typography variant="h6" gutterBottom>
|
|
Supporting Evidence
|
|
</Typography>
|
|
{match.evidenceDetails && match.evidenceDetails.length > 0 ? (
|
|
match.evidenceDetails.map((evidence, evndex) => (
|
|
<Card
|
|
key={evndex}
|
|
variant="outlined"
|
|
sx={{
|
|
mb: 2,
|
|
borderLeft: '4px solid',
|
|
borderColor: theme.palette.primary.main,
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<Typography
|
|
variant="body1"
|
|
component="div"
|
|
sx={{ mb: 1, fontStyle: 'italic' }}
|
|
>
|
|
"{evidence.quote}"
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Relevance: {evidence.context}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Source: {evidence.source}
|
|
</Typography>
|
|
{/* <Chip
|
|
size="small"
|
|
label={`Relevance: ${citation.relevance}%`}
|
|
sx={{
|
|
bgcolor: theme.palette.grey[200],
|
|
}}
|
|
/> */}
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<Typography color="text.secondary">
|
|
No specific evidence found in candidate's profile.
|
|
</Typography>
|
|
)}
|
|
<Typography variant="h6" gutterBottom>
|
|
Skill description
|
|
</Typography>
|
|
<Typography paragraph>{match.description}</Typography>
|
|
{match.status === 'complete' && (
|
|
<Tooltip title="Regenerate Assessment">
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e): void => {
|
|
e.stopPropagation();
|
|
handleAssesmentRegenerate(index, match.skill);
|
|
}}
|
|
>
|
|
<ModelTrainingIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{/* { match.ragResults && match.ragResults.length !== 0 && <>
|
|
<Typography variant="h6" gutterBottom>
|
|
RAG Information
|
|
</Typography>
|
|
<VectorVisualizer inline rag={match.ragResults[0]} />
|
|
</>
|
|
} */}
|
|
</Box>
|
|
)}
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Scrollable>
|
|
);
|
|
};
|
|
|
|
export type { JobAnalysisScore };
|
|
export { JobMatchAnalysis, JobMatchScore };
|