import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Box, Stepper, Step, StepLabel, Button, Typography, Paper, useTheme, Snackbar, Alert, Tabs, Tab, Avatar, useMediaQuery, Divider, } 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'; import { JobInfo } from 'components/ui/JobInfo'; function WorkAddIcon() { return ( ); } interface AnalysisState { job: Job | null; candidate: Candidate | null; analysis: SkillAssessment[] | null; resume: string | null; } interface Step { index: number; label: string; requiredState: string[]; title: string; icon: React.ReactNode; } const initialState: AnalysisState = { job: null, candidate: null, analysis: null, resume: null, }; // Steps in our process const steps: Step[] = [ { requiredState: [], title: 'Job Selection', icon: }, { requiredState: ['job'], title: 'Select Candidate', icon: }, { requiredState: ['job', 'candidate'], title: 'Job Analysis', icon: , }, { requiredState: ['job', 'candidate', 'analysis'], title: 'Generated Resume', icon: , }, ].map((item, index) => { return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') }; }); const capitalize = (str: string) => { return str.charAt(0).toUpperCase() + str.slice(1); }; // Main component const JobAnalysisPage: React.FC = (props: BackstoryPageProps) => { const theme = useTheme(); const { user, guest } = useAuth(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedJob, setSelectedJob } = useSelectedJob(); const [activeStep, setActiveStep] = useState(steps[0]); const [error, setError] = useState(null); const [jobTab, setJobTab] = useState('select'); const [analysisState, setAnalysisState] = useState(null); const [canAdvance, setCanAdvance] = useState(false); const scrollRef = useRef(null); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const canAccessStep = useCallback( (step: Step) => { if (!analysisState) { return; } const missing = step.requiredState.find(f => !(analysisState as any)[f]); return missing; }, [analysisState] ); useEffect(() => { if (analysisState !== null) { return; } const analysis = { ...initialState, candidate: selectedCandidate, job: selectedJob, }; setAnalysisState(analysis); for (let i = steps.length - 1; i >= 0; i--) { const missing = steps[i].requiredState.find(f => !(analysis as any)[f]); if (!missing) { setActiveStep(steps[i]); return; } } }, [analysisState, selectedCandidate, selectedJob, setActiveStep, canAccessStep]); useEffect(() => { if (activeStep.index === steps.length - 1) { setCanAdvance(false); return; } const blocked = canAccessStep(steps[activeStep.index + 1]); if (blocked) { setCanAdvance(false); } else { setCanAdvance(true); } if (scrollRef.current) { scrollRef.current.scrollTo({ top: 0, behavior: 'smooth', }); } }, [setCanAdvance, analysisState, activeStep]); const handleNext = () => { if (activeStep.index === steps.length - 1) { return; } const missing = canAccessStep(steps[activeStep.index + 1]); if (missing) { setError(`${capitalize(missing)} is necessary before continuing.`); return missing; } if (activeStep.index < steps.length - 1) { setActiveStep(prevActiveStep => steps[prevActiveStep.index + 1]); } }; const handleBack = () => { if (activeStep.index === 0) { return; } setActiveStep(prevActiveStep => steps[prevActiveStep.index - 1]); }; const moveToStep = (step: number) => { const missing = canAccessStep(steps[step]); if (missing) { setError(`${capitalize(missing)} is needed to access this step.`); return; } setActiveStep(steps[step]); }; const onCandidateSelect = (candidate: Candidate) => { if (!analysisState) { return; } analysisState.candidate = candidate; setAnalysisState({ ...analysisState }); setSelectedCandidate(candidate); handleNext(); }; const onJobSelect = (job: Job) => { if (!analysisState) { return; } analysisState.job = job; setAnalysisState({ ...analysisState }); setSelectedJob(job); handleNext(); }; // Render function for the candidate selection step const renderCandidateSelection = () => ( ); const handleTabChange = (event: React.SyntheticEvent, value: string) => { setJobTab(value); }; // Render function for the job description step const renderJobDescription = () => { return ( } label="Select Job" /> } label="Create Job" /> {jobTab === 'select' && } {jobTab === 'create' && user && } {jobTab === 'create' && guest && ( )} ); }; const onAnalysisComplete = (skills: SkillAssessment[]) => { if (!analysisState) { return; } analysisState.analysis = skills; setAnalysisState({ ...analysisState }); }; // Render function for the analysis step const renderAnalysis = () => { if (!analysisState) { return; } if (!analysisState.job || !analysisState.candidate) { return ( {JSON.stringify({ job: analysisState.job, candidate: analysisState.candidate, })} ); } return ( ); }; const renderResume = () => { if (!analysisState) { return; } if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) { return <>; } return ( ); }; return ( *:not(.Scrollable)': { flexShrink: 0 /* Prevent shrinking */, }, position: 'relative', }} > {steps.map((step, index) => ( { moveToStep(index); }} slots={{ stepIcon: () => ( = step.index ? theme.palette.primary.main : theme.palette.grey[300], color: 'white', }} > {step.icon} ), }} > {step.title} ))} {analysisState && analysisState.job && ( {!isMobile && ( )} )} {isMobile && } {!isMobile && } {analysisState && analysisState.candidate && ( )} {activeStep.label === 'job-selection' && renderJobDescription()} {activeStep.label === 'select-candidate' && renderCandidateSelection()} {activeStep.label === 'job-analysis' && renderAnalysis()} {activeStep.label === 'generated-resume' && renderResume()} {activeStep.index === steps[steps.length - 1].index ? ( ) : ( )} {/* Error Snackbar */} setError(null)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setError(null)} severity="error" sx={{ width: '100%' }}> {error} ); }; export { JobAnalysisPage };