575 lines
21 KiB
TypeScript
575 lines
21 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Stepper,
|
|
Step,
|
|
StepLabel,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
TextField,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
CardActionArea,
|
|
Avatar,
|
|
Divider,
|
|
CircularProgress,
|
|
Container,
|
|
useTheme,
|
|
Snackbar,
|
|
Alert,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogActions,
|
|
InputAdornment,
|
|
IconButton
|
|
} from '@mui/material';
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import PersonIcon from '@mui/icons-material/Person';
|
|
import WorkIcon from '@mui/icons-material/Work';
|
|
import AssessmentIcon from '@mui/icons-material/Assessment';
|
|
import DescriptionIcon from '@mui/icons-material/Description';
|
|
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
|
import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
|
|
import { Candidate } from "types/types";
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
|
|
|
// Main component
|
|
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|
const theme = useTheme();
|
|
const { user } = useAuth();
|
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
|
|
|
// State management
|
|
const [activeStep, setActiveStep] = useState(0);
|
|
const [jobDescription, setJobDescription] = useState('');
|
|
const [jobTitle, setJobTitle] = useState('');
|
|
const [jobLocation, setJobLocation] = useState('');
|
|
const [analysisStarted, setAnalysisStarted] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [openUploadDialog, setOpenUploadDialog] = useState(false);
|
|
const { apiClient } = useAuth();
|
|
const { setSnack } = props;
|
|
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (candidates !== null || selectedCandidate) {
|
|
return;
|
|
}
|
|
const getCandidates = async () => {
|
|
try {
|
|
const results = await apiClient.getCandidates();
|
|
const candidates: Candidate[] = results.data;
|
|
candidates.sort((a, b) => {
|
|
let result = a.lastName.localeCompare(b.lastName);
|
|
if (result === 0) {
|
|
result = a.firstName.localeCompare(b.firstName);
|
|
}
|
|
if (result === 0) {
|
|
result = a.username.localeCompare(b.username);
|
|
}
|
|
return result;
|
|
});
|
|
console.log(candidates);
|
|
setCandidates(candidates);
|
|
} catch (err) {
|
|
setSnack("" + err);
|
|
}
|
|
};
|
|
|
|
getCandidates();
|
|
}, [candidates, setSnack]);
|
|
|
|
useEffect(() => {
|
|
if (selectedCandidate && activeStep === 0) {
|
|
setActiveStep(1);
|
|
}
|
|
}, [selectedCandidate, activeStep]);
|
|
|
|
// Steps in our process
|
|
const steps = selectedCandidate === null ? [
|
|
{ index: 0, label: 'Select Candidate', icon: <PersonIcon /> },
|
|
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
|
|
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
|
|
] : [
|
|
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
|
|
{ index: 2, label: 'View Analysis', icon: <AssessmentIcon /> }
|
|
];
|
|
|
|
// Mock handlers for our analysis APIs
|
|
const fetchRequirements = async (): Promise<string[]> => {
|
|
// Simulates extracting requirements from the job description
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// This would normally parse the job description to extract requirements
|
|
const mockRequirements = [
|
|
"5+ years of React development experience",
|
|
"Strong TypeScript skills",
|
|
"Experience with RESTful APIs",
|
|
"Knowledge of state management solutions (Redux, Context API)",
|
|
"Experience with CI/CD pipelines",
|
|
"Cloud platform experience (AWS, Azure, GCP)"
|
|
];
|
|
|
|
return mockRequirements;
|
|
};
|
|
|
|
const fetchMatchForRequirement = async (requirement: string): Promise<any> => {
|
|
// Create different mock responses based on the requirement
|
|
const mockResponses: Record<string, any> = {
|
|
"5+ years of React development experience": {
|
|
requirement: "5+ years of React development experience",
|
|
status: "complete",
|
|
matchScore: 85,
|
|
assessment: "The candidate demonstrates extensive React experience spanning over 6 years, with a strong portfolio of complex applications and deep understanding of React's component lifecycle and hooks.",
|
|
citations: [
|
|
{
|
|
text: "Led frontend development team of 5 engineers to rebuild our customer portal using React and TypeScript, resulting in 40% improved performance and 30% reduction in bugs.",
|
|
source: "Resume, Work Experience",
|
|
relevance: 95
|
|
},
|
|
{
|
|
text: "Developed and maintained reusable React component library used across 12 different products within the organization.",
|
|
source: "Resume, Work Experience",
|
|
relevance: 90
|
|
},
|
|
{
|
|
text: "I've been working with React since 2017, building everything from small widgets to enterprise applications.",
|
|
source: "Cover Letter",
|
|
relevance: 85
|
|
}
|
|
]
|
|
},
|
|
"Strong TypeScript skills": {
|
|
requirement: "Strong TypeScript skills",
|
|
status: "complete",
|
|
matchScore: 90,
|
|
assessment: "The candidate shows excellent TypeScript proficiency through their work history and personal projects. They have implemented complex type systems and demonstrate an understanding of advanced TypeScript features.",
|
|
citations: [
|
|
{
|
|
text: "Converted a legacy JavaScript codebase of 100,000+ lines to TypeScript, implementing strict type checking and reducing runtime errors by 70%.",
|
|
source: "Resume, Projects",
|
|
relevance: 98
|
|
},
|
|
{
|
|
text: "Created comprehensive TypeScript interfaces for our GraphQL API, ensuring type safety across the entire application stack.",
|
|
source: "Resume, Technical Skills",
|
|
relevance: 95
|
|
}
|
|
]
|
|
},
|
|
"Experience with RESTful APIs": {
|
|
requirement: "Experience with RESTful APIs",
|
|
status: "complete",
|
|
matchScore: 75,
|
|
assessment: "The candidate has good experience with RESTful APIs, having both consumed and designed them. They understand REST principles but have less documented experience with API versioning and caching strategies.",
|
|
citations: [
|
|
{
|
|
text: "Designed and implemented a RESTful API serving over 1M requests daily with a focus on performance and scalability.",
|
|
source: "Resume, Technical Projects",
|
|
relevance: 85
|
|
},
|
|
{
|
|
text: "Worked extensively with third-party APIs including Stripe, Twilio, and Salesforce to integrate payment processing and communication features.",
|
|
source: "Resume, Work Experience",
|
|
relevance: 70
|
|
}
|
|
]
|
|
},
|
|
"Knowledge of state management solutions (Redux, Context API)": {
|
|
requirement: "Knowledge of state management solutions (Redux, Context API)",
|
|
status: "complete",
|
|
matchScore: 65,
|
|
assessment: "The candidate has moderate experience with state management, primarily using Redux. There is less evidence of Context API usage, which could indicate a knowledge gap in more modern React state management approaches.",
|
|
citations: [
|
|
{
|
|
text: "Implemented Redux for global state management in an e-commerce application, handling complex state logic for cart, user preferences, and product filtering.",
|
|
source: "Resume, Skills",
|
|
relevance: 80
|
|
},
|
|
{
|
|
text: "My experience includes working with state management libraries like Redux and MobX.",
|
|
source: "Cover Letter",
|
|
relevance: 60
|
|
}
|
|
]
|
|
},
|
|
"Experience with CI/CD pipelines": {
|
|
requirement: "Experience with CI/CD pipelines",
|
|
status: "complete",
|
|
matchScore: 40,
|
|
assessment: "The candidate shows limited experience with CI/CD pipelines. While they mention some exposure to Jenkins and GitLab CI, there is insufficient evidence of setting up or maintaining comprehensive CI/CD workflows.",
|
|
citations: [
|
|
{
|
|
text: "Familiar with CI/CD tools including Jenkins and GitLab CI.",
|
|
source: "Resume, Skills",
|
|
relevance: 40
|
|
}
|
|
]
|
|
},
|
|
"Cloud platform experience (AWS, Azure, GCP)": {
|
|
requirement: "Cloud platform experience (AWS, Azure, GCP)",
|
|
status: "complete",
|
|
matchScore: 30,
|
|
assessment: "The candidate demonstrates minimal experience with cloud platforms. There is a brief mention of AWS S3 and Lambda, but no substantial evidence of deeper cloud architecture knowledge or experience with Azure or GCP.",
|
|
citations: [
|
|
{
|
|
text: "Used AWS S3 for file storage and Lambda for image processing in a photo sharing application.",
|
|
source: "Resume, Projects",
|
|
relevance: 35
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
// Return a promise that resolves with the mock data after a delay
|
|
return new Promise((resolve) => {
|
|
// Different requirements resolve at different speeds to simulate real-world analysis
|
|
const delay = Math.random() * 5000 + 2000; // 2-7 seconds
|
|
setTimeout(() => {
|
|
resolve(mockResponses[requirement]);
|
|
}, delay);
|
|
});
|
|
};
|
|
|
|
// Navigation handlers
|
|
const handleNext = () => {
|
|
if (activeStep === 0 && !selectedCandidate) {
|
|
setError('Please select a candidate before continuing.');
|
|
return;
|
|
}
|
|
|
|
if (activeStep === 1 && (/*(extraInfo && !jobTitle) || */!jobDescription)) {
|
|
setError('Please provide both job title and description before continuing.');
|
|
return;
|
|
}
|
|
|
|
if (activeStep === 2) {
|
|
setAnalysisStarted(true);
|
|
}
|
|
|
|
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
// setActiveStep(0);
|
|
setActiveStep(1);
|
|
// setSelectedCandidate(null);
|
|
setJobDescription('');
|
|
// setJobTitle('');
|
|
// setJobLocation('');
|
|
setAnalysisStarted(false);
|
|
};
|
|
|
|
// Render function for the candidate selection step
|
|
const renderCandidateSelection = () => (
|
|
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Select a Candidate
|
|
</Typography>
|
|
|
|
{/* <Box sx={{ mb: 3, display: 'flex' }}>
|
|
<TextField
|
|
fullWidth
|
|
variant="outlined"
|
|
placeholder="Search candidates by name, title, or location"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton onClick={handleSearch} edge="end">
|
|
<SearchIcon />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{ mr: 2 }}
|
|
/>
|
|
</Box> */}
|
|
|
|
<Grid container spacing={3}>
|
|
{candidates?.map((candidate) => (
|
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={candidate.id}>
|
|
<Card
|
|
elevation={selectedCandidate?.id === candidate.id ? 8 : 1}
|
|
sx={{
|
|
height: '100%',
|
|
borderColor: selectedCandidate?.id === candidate.id ? theme.palette.primary.main : 'transparent',
|
|
borderWidth: 2,
|
|
borderStyle: 'solid',
|
|
transition: 'all 0.3s ease'
|
|
}}
|
|
>
|
|
<CardActionArea
|
|
onClick={() => setSelectedCandidate(candidate)}
|
|
sx={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
|
|
>
|
|
<CardContent sx={{ flexGrow: 1, p: 3 }}>
|
|
<Box sx={{ display: 'flex', mb: 2, alignItems: 'center' }}>
|
|
<Avatar
|
|
src={candidate.profileImage}
|
|
alt={candidate.firstName}
|
|
sx={{ width: 64, height: 64, mr: 2 }}
|
|
/>
|
|
<Box>
|
|
<Typography variant="h6" component="div">
|
|
{candidate.fullName}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{candidate.description}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Divider sx={{ my: 2 }} />
|
|
|
|
{candidate.location && <Typography variant="body2" sx={{ mb: 1 }}>
|
|
<strong>Location:</strong> {candidate.location.country}
|
|
</Typography>}
|
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
<strong>Email:</strong> {candidate.email}
|
|
</Typography>
|
|
{candidate.phone && <Typography variant="body2">
|
|
<strong>Phone:</strong> {candidate.phone}
|
|
</Typography>}
|
|
</CardContent>
|
|
</CardActionArea>
|
|
</Card>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Paper>
|
|
);
|
|
|
|
const extraInfo = false;
|
|
|
|
// Render function for the job description step
|
|
const renderJobDescription = () => (
|
|
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
|
{extraInfo && <>
|
|
<Typography variant="h5" gutterBottom>
|
|
Enter Job Details
|
|
</Typography>
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="Job Title"
|
|
variant="outlined"
|
|
value={jobTitle}
|
|
onChange={(e) => setJobTitle(e.target.value)}
|
|
required
|
|
margin="normal"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="Job Location"
|
|
variant="outlined"
|
|
value={jobLocation}
|
|
onChange={(e) => setJobLocation(e.target.value)}
|
|
margin="normal"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
</>
|
|
}
|
|
|
|
<Grid size={{ xs: 12 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
|
|
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
|
Job Description
|
|
</Typography>
|
|
{extraInfo && <Button
|
|
variant="outlined"
|
|
startIcon={<FileUploadIcon />}
|
|
size="small"
|
|
onClick={() => setOpenUploadDialog(true)}
|
|
>
|
|
Upload
|
|
</Button>}
|
|
</Box>
|
|
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={12}
|
|
placeholder="Enter the job description here..."
|
|
variant="outlined"
|
|
value={jobDescription}
|
|
onChange={(e) => setJobDescription(e.target.value)}
|
|
required
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start" sx={{ alignSelf: 'flex-start', mt: 1.5 }}>
|
|
<DescriptionIcon color="action" />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
|
The job description will be used to extract requirements for candidate matching.
|
|
</Typography>
|
|
</Grid>
|
|
</Paper>
|
|
);
|
|
|
|
// Render function for the analysis step
|
|
const renderAnalysis = () => (
|
|
<Box sx={{ mt: 3 }}>
|
|
{selectedCandidate && (
|
|
<JobMatchAnalysis
|
|
jobTitle={jobTitle}
|
|
candidateName={selectedCandidate.fullName}
|
|
fetchRequirements={fetchRequirements}
|
|
fetchMatchForRequirement={fetchMatchForRequirement}
|
|
/>
|
|
)}
|
|
</Box>
|
|
);
|
|
|
|
// If no user is logged in, show message
|
|
if (!user) {
|
|
return (
|
|
<Container maxWidth="md">
|
|
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Please log in to access candidate analysis
|
|
</Typography>
|
|
<Button variant="contained" color="primary" sx={{ mt: 2 }}>
|
|
Log In
|
|
</Button>
|
|
</Paper>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Container maxWidth="lg">
|
|
<Paper elevation={1} sx={{ p: 3, mt: 3, borderRadius: 2 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
Candidate Analysis
|
|
</Typography>
|
|
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
|
Match candidates to job requirements with AI-powered analysis
|
|
</Typography>
|
|
</Paper>
|
|
|
|
<Box sx={{ mt: 4, mb: 4 }}>
|
|
<Stepper activeStep={activeStep} alternativeLabel>
|
|
{steps.map(step => (
|
|
<Step key={step.index}>
|
|
<StepLabel slots={{
|
|
stepIcon: () => (
|
|
<Avatar
|
|
sx={{
|
|
bgcolor: activeStep >= step.index ? theme.palette.primary.main : theme.palette.grey[300],
|
|
color: 'white'
|
|
}}
|
|
>
|
|
{step.icon}
|
|
</Avatar>
|
|
)
|
|
}}
|
|
>
|
|
{step.label}
|
|
</StepLabel>
|
|
</Step>
|
|
))}
|
|
</Stepper>
|
|
</Box>
|
|
|
|
{activeStep === 0 && renderCandidateSelection()}
|
|
{activeStep === 1 && renderJobDescription()}
|
|
{activeStep === 2 && renderAnalysis()}
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
|
|
<Button
|
|
color="inherit"
|
|
disabled={activeStep === steps[0].index}
|
|
onClick={handleBack}
|
|
sx={{ mr: 1 }}
|
|
>
|
|
Back
|
|
</Button>
|
|
<Box sx={{ flex: '1 1 auto' }} />
|
|
|
|
{activeStep === steps[steps.length - 1].index ? (
|
|
<Button onClick={handleReset} variant="outlined">
|
|
Start New Analysis
|
|
</Button>
|
|
) : (
|
|
<Button onClick={handleNext} variant="contained">
|
|
{activeStep === steps[steps.length - 1].index - 1 ? 'Start Analysis' : 'Next'}
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Error Snackbar */}
|
|
<Snackbar
|
|
open={!!error}
|
|
autoHideDuration={6000}
|
|
onClose={() => setError(null)}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
>
|
|
<Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}>
|
|
{error}
|
|
</Alert>
|
|
</Snackbar>
|
|
|
|
{/* Upload Dialog */}
|
|
<Dialog open={openUploadDialog} onClose={() => setOpenUploadDialog(false)}>
|
|
<DialogTitle>Upload Job Description</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText>
|
|
Upload a job description document (.pdf, .docx, .txt, or .md)
|
|
</DialogContentText>
|
|
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
|
<Button
|
|
variant="outlined"
|
|
component="label"
|
|
startIcon={<FileUploadIcon />}
|
|
sx={{ mt: 1 }}
|
|
>
|
|
Choose File
|
|
<input
|
|
type="file"
|
|
hidden
|
|
accept=".pdf,.docx,.txt,.md"
|
|
onChange={() => {
|
|
// This would handle file upload in a real application
|
|
setOpenUploadDialog(false);
|
|
// Mock setting job description from file
|
|
setJobDescription(
|
|
"Senior Frontend Developer\n\nRequired Skills:\n- 5+ years of React development experience\n- Strong TypeScript skills\n- Experience with RESTful APIs\n- Knowledge of state management solutions (Redux, Context API)\n- Experience with CI/CD pipelines\n- Cloud platform experience (AWS, Azure, GCP)\n\nResponsibilities:\n- Develop and maintain frontend applications using React and TypeScript\n- Collaborate with backend developers to integrate APIs\n- Optimize applications for maximum speed and scalability\n- Design and implement new features and functionality\n- Ensure the technical feasibility of UI/UX designs"
|
|
);
|
|
setJobTitle("Senior Frontend Developer");
|
|
setJobLocation("Remote");
|
|
}}
|
|
/>
|
|
</Button>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setOpenUploadDialog(false)}>Cancel</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export { JobAnalysisPage }; |