325 lines
9.5 KiB
TypeScript
325 lines
9.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Stepper,
|
|
Step,
|
|
StepLabel,
|
|
Button,
|
|
Typography,
|
|
Paper,
|
|
useTheme,
|
|
Snackbar,
|
|
Alert,
|
|
Tabs,
|
|
Tab,
|
|
Avatar,
|
|
} from '@mui/material';
|
|
import {
|
|
Add,
|
|
WorkOutline,
|
|
} from '@mui/icons-material';
|
|
import PersonIcon from '@mui/icons-material/Person';
|
|
import WorkIcon from '@mui/icons-material/Work';
|
|
import AssessmentIcon from '@mui/icons-material/Assessment';
|
|
import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
|
|
import { Candidate, Job, SkillAssessment } from "types/types";
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
|
import { ComingSoon } from 'components/ui/ComingSoon';
|
|
import { LoginRequired } from 'components/ui/LoginRequired';
|
|
import { Scrollable } from 'components/Scrollable';
|
|
import { CandidatePicker } from 'components/ui/CandidatePicker';
|
|
import { JobPicker } from 'components/ui/JobPicker';
|
|
import { JobCreator } from 'components/JobCreator';
|
|
import { LoginRestricted } from 'components/ui/LoginRestricted';
|
|
import JsonView from '@uiw/react-json-view';
|
|
import { ResumeGenerator } from 'components/ResumeGenerator';
|
|
|
|
function WorkAddIcon() {
|
|
return (
|
|
<Box position="relative" display="inline-flex"
|
|
sx={{
|
|
lineHeight: "30px",
|
|
mb: "6px",
|
|
}}
|
|
>
|
|
<WorkOutline sx={{ fontSize: 24 }} />
|
|
<Add
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: -2,
|
|
right: -2,
|
|
fontSize: 14,
|
|
bgcolor: 'background.paper',
|
|
borderRadius: '50%',
|
|
boxShadow: 1,
|
|
}}
|
|
color="primary"
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Main component
|
|
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|
const theme = useTheme();
|
|
const { user, guest } = useAuth();
|
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
|
// State management
|
|
const [activeStep, setActiveStep] = useState(0);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [jobTab, setJobTab] = useState<string>('load');
|
|
const [skills, setSkills] = useState<SkillAssessment[] | null>(null)
|
|
useEffect(() => {
|
|
if (!selectedCandidate) {
|
|
if (activeStep !== 0) {
|
|
setActiveStep(0);
|
|
}
|
|
} else if (!selectedJob) {
|
|
if (activeStep !== 1) {
|
|
setActiveStep(1);
|
|
}
|
|
}
|
|
}, [selectedCandidate, selectedJob, activeStep])
|
|
|
|
// Steps in our process
|
|
const steps = [
|
|
{ index: 0, label: 'Select Candidate', icon: <PersonIcon /> },
|
|
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
|
|
{ index: 2, label: 'Job Analysis', icon: <WorkIcon /> },
|
|
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
|
|
];
|
|
|
|
// Navigation handlers
|
|
const handleNext = () => {
|
|
if (activeStep === 0 && !selectedCandidate) {
|
|
setError('Please select a candidate before continuing.');
|
|
return;
|
|
}
|
|
|
|
if (activeStep === 1 && !selectedJob) {
|
|
setError('Please select a job before continuing.');
|
|
return;
|
|
}
|
|
|
|
if (activeStep === 2 && !skills) {
|
|
setError('Skill assessment must be complete before continuing.');
|
|
return;
|
|
}
|
|
|
|
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
console.log(activeStep);
|
|
if (activeStep === 1) {
|
|
setSelectedCandidate(null);
|
|
}
|
|
if (activeStep === 2) {
|
|
setSelectedJob(null);
|
|
}
|
|
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
|
};
|
|
|
|
const moveToStep = (step: number) => {
|
|
console.log(`Move to ${step}`)
|
|
switch (step) {
|
|
case 0: /* Select candidate */
|
|
setSelectedCandidate(null);
|
|
setSelectedJob(null);
|
|
setSkills(null);
|
|
break;
|
|
case 1: /* Select Job */
|
|
setSelectedJob(null);
|
|
setSkills(null);
|
|
break;
|
|
case 2: /* Job Analysis */
|
|
setSkills(null);
|
|
break;
|
|
case 3: /* Generate Resume */
|
|
break;
|
|
}
|
|
setActiveStep(step);
|
|
}
|
|
|
|
const onCandidateSelect = (candidate: Candidate) => {
|
|
setSelectedCandidate(candidate);
|
|
setActiveStep(1);
|
|
}
|
|
|
|
const onJobSelect = (job: Job) => {
|
|
setSelectedJob(job)
|
|
setActiveStep(2);
|
|
}
|
|
|
|
// 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>
|
|
|
|
<CandidatePicker onSelect={onCandidateSelect} />
|
|
</Paper>
|
|
);
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
|
setJobTab(value);
|
|
};
|
|
|
|
// Render function for the job description step
|
|
const renderJobDescription = () => {
|
|
if (!selectedCandidate) {
|
|
return;
|
|
}
|
|
|
|
return (<Box sx={{ mt: 3 }}>
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
|
<Tabs value={jobTab} onChange={handleTabChange} centered>
|
|
<Tab value='load' icon={<WorkOutline />} label="Load" />
|
|
<Tab value='create' icon={<WorkAddIcon />} label="Create" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{jobTab === 'load' &&
|
|
<JobPicker onSelect={onJobSelect} />
|
|
}
|
|
{jobTab === 'create' && user &&
|
|
<JobCreator
|
|
onSave={onJobSelect}
|
|
/>}
|
|
{jobTab === 'create' && guest &&
|
|
<LoginRestricted><JobCreator
|
|
onSave={onJobSelect}
|
|
/></LoginRestricted>}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const onAnalysisComplete = (skills: SkillAssessment[]) => {
|
|
setSkills(skills);
|
|
};
|
|
|
|
// Render function for the analysis step
|
|
const renderAnalysis = () => (
|
|
<Box sx={{ mt: 3 }}>
|
|
{selectedCandidate && selectedJob && (
|
|
<JobMatchAnalysis
|
|
job={selectedJob}
|
|
candidate={selectedCandidate}
|
|
onAnalysisComplete={onAnalysisComplete}
|
|
/>
|
|
)}
|
|
</Box>
|
|
);
|
|
|
|
const renderResume = () => (
|
|
<Box sx={{ mt: 3 }}>
|
|
{skills && selectedCandidate && selectedJob &&
|
|
<ResumeGenerator
|
|
job={selectedJob}
|
|
candidate={selectedCandidate}
|
|
skills={skills}
|
|
/>}
|
|
</Box>
|
|
);
|
|
|
|
return (
|
|
<Box sx={{
|
|
display: "flex", flexDirection: "column",
|
|
height: "100%", /* Restrict to main-container's height */
|
|
width: "100%",
|
|
minHeight: 0,/* Prevent flex overflow */
|
|
maxHeight: "min-content",
|
|
"& > *:not(.Scrollable)": {
|
|
flexShrink: 0, /* Prevent shrinking */
|
|
},
|
|
position: "relative",
|
|
}}>
|
|
<Paper elevation={4} sx={{ m: 0, borderRadius: 0, mb: 1, p: 0 }}>{selectedCandidate && <CandidateInfo variant="small" candidate={selectedCandidate} sx={{ width: "100%" }} />}</Paper>
|
|
<Scrollable
|
|
sx={{
|
|
position: "relative",
|
|
maxHeight: "100%",
|
|
width: "100%",
|
|
display: "flex", flexGrow: 1,
|
|
flex: 1, /* Take remaining space in some-container */
|
|
overflowY: "auto", /* Scroll if content overflows */
|
|
}}>
|
|
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
|
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
|
Match candidates to job requirements with AI-powered analysis
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ mt: 4, mb: 4 }}>
|
|
<Stepper activeStep={activeStep} alternativeLabel>
|
|
{steps.map((step, index) => (
|
|
<Step>
|
|
<StepLabel sx={{ cursor: "pointer" }} onClick={() => { moveToStep(index); }}
|
|
slots={{
|
|
stepIcon: () => (
|
|
<Avatar key={step.index}
|
|
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()}
|
|
</Scrollable>
|
|
|
|
<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={() => { moveToStep(0) }} variant="outlined">
|
|
Start New Analysis
|
|
</Button>
|
|
) : (
|
|
<Button onClick={handleNext} variant="contained">
|
|
{activeStep === steps.length - 1 ? 'Done' : '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>
|
|
</Box>);
|
|
};
|
|
|
|
export { JobAnalysisPage }; |