backstory/frontend/src/pages/JobAnalysisPage.tsx

462 lines
15 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';
import { CandidateInfo } from 'components/CandidateInfo';
import { ComingSoon } from 'components/ui/ComingSoon';
// Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const theme = useTheme();
const { user } = useAuth();
const navigate = useNavigate();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
// State management
const [activeStep, setActiveStep] = useState(0);
const [jobDescription, setJobDescription] = useState('');
const [jobTitle, setJobTitle] = useState('');
const [company, setCompany] = 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 [candidates, setCandidates] = useState<Candidate[] | null>(null);
const user_type = user?.userType || 'guest';
const user_id = user?.id || '';
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 = [
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
];
if (!selectedCandidate) {
steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> })
}
// Navigation handlers
const handleNext = () => {
if (activeStep === 0 && !selectedCandidate) {
setError('Please select a candidate before continuing.');
return;
}
if (activeStep === 1) {
if (!jobDescription) {
setError('Please provide job 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>
);
// Render function for the job description step
const renderJobDescription = () => (
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
<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="Company"
variant="outlined"
value={company}
onChange={(e) => setCompany(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 Selection
</Typography>
<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
job={{ title: jobTitle, description: jobDescription, company: company, ownerId: user_id, ownerType: user_type }}
candidate={selectedCandidate}
{...backstoryProps}
/>
)}
</Box>
);
const renderResume = () => (
<Box sx={{ mt: 3 }}>
{selectedCandidate && <ComingSoon>Resume Builder</ComingSoon>}
</Box>
);
// If no user is logged in, show message
if (!user?.id) {
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" onClick={() => { navigate('/login'); }} 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>
{selectedCandidate && <CandidateInfo variant="small" candidate={selectedCandidate} />}
<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()}
{activeStep === 3 && renderResume()}
<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 };