Integrating new resume generator

This commit is contained in:
James Ketr 2025-06-09 11:57:12 -07:00
parent 1fbc5317d3
commit dd0ab5eda6
5 changed files with 103 additions and 40 deletions

View File

@ -34,6 +34,7 @@ import * as Types from 'types/types';
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
job: Job; job: Job;
candidate: Candidate; candidate: Candidate;
onAnalysisComplete: (skills: SkillAssessment[]) => void;
} }
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
@ -50,6 +51,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const { const {
job, job,
candidate, candidate,
onAnalysisComplete,
} = props } = props
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -138,7 +140,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
return; return;
} }
const fetchMatchData = async () => { const fetchMatchData = async (skills: SkillAssessment[]) => {
if (requirements.length === 0) return; if (requirements.length === 0) return;
// Process requirements one by one // Process requirements one by one
@ -153,6 +155,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const request: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement, skillMatchHandlers); const request: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement, skillMatchHandlers);
const result = await request.promise; const result = await request.promise;
const skillMatch = result.skillAssessment; const skillMatch = result.skillAssessment;
skills.push(skillMatch);
setMatchStatus(''); setMatchStatus('');
let matchScore: number = 0; let matchScore: number = 0;
switch (skillMatch.evidenceStrength.toUpperCase()) { switch (skillMatch.evidenceStrength.toUpperCase()) {
@ -201,8 +204,13 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}; };
setAnalyzing(true); setAnalyzing(true);
fetchMatchData().then(() => { setAnalyzing(false); setStartAnalysis(false) }); const skills: SkillAssessment[] = [];
}, [job, startAnalysis, analyzing, requirements, loadingRequirements]); fetchMatchData(skills).then(() => {
setAnalyzing(false);
setStartAnalysis(false);
onAnalysisComplete && onAnalysisComplete(skills);
});
}, [job, onAnalysisComplete, startAnalysis, analyzing, requirements, loadingRequirements]);
// Get color based on match score // Get color based on match score
const getMatchColor = (score: number): string => { const getMatchColor = (score: number): string => {
@ -270,7 +278,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Grid size={{ xs: 12 }} sx={{ mt: 2 }}> <Grid size={{ xs: 12 }} sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}>
{<Button disabled={analyzing || startAnalysis} onClick={beginAnalysis} variant="contained">Start Analysis</Button>} {<Button disabled={analyzing || startAnalysis} onClick={beginAnalysis} variant="contained">Start Skill Assessment</Button>}
{overallScore !== 0 && <> {overallScore !== 0 && <>
<Typography variant="h5" component="h2" sx={{ mr: 2 }}> <Typography variant="h5" component="h2" sx={{ mr: 2 }}>
Overall Match: Overall Match:

View File

@ -0,0 +1,51 @@
import React, { useState, useCallback, useRef } from 'react';
import {
Tabs,
Tab,
Box,
Button,
} from '@mui/material';
import { Job, Candidate, SkillAssessment } from "types/types";
import JsonView from '@uiw/react-json-view';
interface ResumeGeneratorProps {
job: Job;
candidate: Candidate;
skills: SkillAssessment[];
onComplete?: (resume: string) => void;
}
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
const { job, candidate, skills, onComplete } = props;
const [resume, setResume] = useState<string>('Generated resume goes here...');
const [generating, setGenerating] = useState<boolean>(false);
// State for editing job description
const generateResume = () => {
setResume('Generation begins...');
setGenerating(true);
setTimeout(() => {
setGenerating(false);
setResume('Generation complete');
onComplete && onComplete(resume);
}, 3000);
};
return (
<Box
className="ResumeGenerator"
sx={{display: "flex", flexDirection: "row", width: "100%"}}>
<JsonView value={skills}/>
<Box sx={{display: "flex", flexDirection: "column"}}>
<Box>{resume}</Box>
<Button disabled={generating} onClick={generateResume} variant="contained">Generate Resume</Button>
</Box>
</Box>
)
};
export {
ResumeGenerator
};

View File

@ -50,17 +50,6 @@ const CandidatePicker = (props: CandidatePickerProps) => {
return ( return (
<Box sx={{display: "flex", flexDirection: "column"}}> <Box sx={{display: "flex", flexDirection: "column"}}>
{user?.isAdmin &&
<Box sx={{ p: 1, textAlign: "center" }}>
Not seeing a candidate you like?
<Button
variant="contained"
sx={{ m: 1 }}
onClick={() => { navigate('/generate-candidate') }}>
Generate your own perfect AI candidate!
</Button>
</Box>
}
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}> <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
{candidates?.map((u, i) => {candidates?.map((u, i) =>
<Box key={`${u.username}`} <Box key={`${u.username}`}

View File

@ -72,20 +72,23 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<strong>Title:</strong> {job.title} <strong>Title:</strong> {job.title}
</Typography> </Typography>
} }
{/* {job.datePosted && {job.company &&
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Posted:</strong> {job.datePosted.toISOString()} <strong>Company:</strong> {job.company}
</Typography> </Typography>
} */} }
{job.company && {job.summary && <Typography variant="body2">
<Typography variant="body2" sx={{ mb: 1 }}> <strong>Summary:</strong> {job.summary}
<strong>Company:</strong> {job.company} </Typography>
</Typography> }
} {job.createdAt && <Typography variant="body2">
{job.summary && <Typography variant="body2"> <strong>Created:</strong> {job.createdAt.toISOString()}
<strong>Summary:</strong> {job.summary} </Typography>
</Typography> }
} { job.owner && <Typography variant="body2">
<strong>Created by:</strong> {job.owner.fullName}
</Typography>
}
</>} </>}
</CardContent> </CardContent>
<CardActions> <CardActions>

View File

@ -22,7 +22,7 @@ import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from '@mui/icons-material/Work'; import WorkIcon from '@mui/icons-material/Work';
import AssessmentIcon from '@mui/icons-material/Assessment'; import AssessmentIcon from '@mui/icons-material/Assessment';
import { JobMatchAnalysis } from 'components/JobMatchAnalysis'; import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate, Job } from "types/types"; import { Candidate, Job, SkillAssessment } from "types/types";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -35,6 +35,8 @@ import { CandidatePicker } from 'components/ui/CandidatePicker';
import { JobPicker } from 'components/ui/JobPicker'; import { JobPicker } from 'components/ui/JobPicker';
import { JobCreator } from 'components/JobCreator'; import { JobCreator } from 'components/JobCreator';
import { LoginRestricted } from 'components/ui/LoginRestricted'; import { LoginRestricted } from 'components/ui/LoginRestricted';
import JsonView from '@uiw/react-json-view';
import { ResumeGenerator } from 'components/ResumeGenerator';
function WorkAddIcon() { function WorkAddIcon() {
return ( return (
@ -65,19 +67,14 @@ function WorkAddIcon() {
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const theme = useTheme(); const theme = useTheme();
const { user, guest } = useAuth(); const { user, guest } = useAuth();
const navigate = useNavigate();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate() const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
const { selectedJob, setSelectedJob } = useSelectedJob(); const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = useAppState();
// State management // State management
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [analysisStarted, setAnalysisStarted] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>('load'); const [jobTab, setJobTab] = useState<string>('load');
const [skills, setSkills] = useState<SkillAssessment[] | null>(null)
useEffect(() => { useEffect(() => {
console.log({ activeStep, selectedCandidate, selectedJob });
if (!selectedCandidate) { if (!selectedCandidate) {
if (activeStep !== 0) { if (activeStep !== 0) {
setActiveStep(0); setActiveStep(0);
@ -109,8 +106,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return; return;
} }
if (activeStep === 2) { if (activeStep === 2 && !skills) {
setAnalysisStarted(true); setError('Skill assessment must be complete before continuing.');
return;
} }
setActiveStep((prevActiveStep) => prevActiveStep + 1); setActiveStep((prevActiveStep) => prevActiveStep + 1);
@ -128,15 +126,19 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
}; };
const moveToStep = (step: number) => { const moveToStep = (step: number) => {
console.log(`Move to ${step}`)
switch (step) { switch (step) {
case 0: /* Select candidate */ case 0: /* Select candidate */
setSelectedCandidate(null); setSelectedCandidate(null);
setSelectedJob(null); setSelectedJob(null);
setSkills(null);
break; break;
case 1: /* Select Job */ case 1: /* Select Job */
setSelectedJob(null); setSelectedJob(null);
setSkills(null);
break; break;
case 2: /* Job Analysis */ case 2: /* Job Analysis */
setSkills(null);
break; break;
case 3: /* Generate Resume */ case 3: /* Generate Resume */
break; break;
@ -198,6 +200,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
); );
} }
const onAnalysisComplete = (skills: SkillAssessment[]) => {
setSkills(skills);
};
// Render function for the analysis step // Render function for the analysis step
const renderAnalysis = () => ( const renderAnalysis = () => (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
@ -205,6 +211,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<JobMatchAnalysis <JobMatchAnalysis
job={selectedJob} job={selectedJob}
candidate={selectedCandidate} candidate={selectedCandidate}
onAnalysisComplete={onAnalysisComplete}
/> />
)} )}
</Box> </Box>
@ -212,7 +219,12 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const renderResume = () => ( const renderResume = () => (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
{selectedCandidate && <ComingSoon>Resume Builder</ComingSoon>} {skills && selectedCandidate && selectedJob &&
<ResumeGenerator
job={selectedJob}
candidate={selectedCandidate}
skills={skills}
/>}
</Box> </Box>
); );
@ -298,7 +310,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Button> </Button>
) : ( ) : (
<Button onClick={handleNext} variant="contained"> <Button onClick={handleNext} variant="contained">
{activeStep === steps[steps.length - 1].index - 1 ? 'Done' : 'Next'} {activeStep === steps.length - 1 ? 'Done' : 'Next'}
</Button> </Button>
)} )}
</Box> </Box>