Integrating new resume generator
This commit is contained in:
parent
1fbc5317d3
commit
dd0ab5eda6
@ -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:
|
||||||
|
51
frontend/src/components/ResumeGenerator.tsx
Normal file
51
frontend/src/components/ResumeGenerator.tsx
Normal 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
|
||||||
|
};
|
||||||
|
|
@ -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}`}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user