Compare commits

..

No commits in common. "5fed56ba76e3a26262eb1477434bbacfd0b51403" and "b76141f3d12595657024fab217eb2587e69b9c98" have entirely different histories.

6 changed files with 237 additions and 188 deletions

View File

@ -354,20 +354,13 @@ const JobManagement = (props: BackstoryElementProps) => {
// This would call your API to extract requirements from the job description // This would call your API to extract requirements from the job description
}; };
const loadJob = async () => { const renderJobCreation = () => {
const job = await apiClient.getJob("7594e989-a926-45a2-9b07-ae553d2e0d0d"); if (!user) {
setSelectedJob(job); return <Box>You must be logged in</Box>;
} }
const renderJobCreation = () => {
if (!user) {
return <Box>You must be logged in</Box>;
}
return ( return (
<Box sx={{ <Box sx={{ maxWidth: 1200, mx: 'auto', p: { xs: 2, sm: 3 } }}>
mx: 'auto', p: { xs: 2, sm: 3 },
}}>
<Button onClick={loadJob} variant="contained">Load Job</Button>
{/* Upload Section */} {/* Upload Section */}
<Card elevation={3} sx={{ mb: 4 }}> <Card elevation={3} sx={{ mb: 4 }}>
<CardHeader <CardHeader
@ -544,10 +537,10 @@ const JobManagement = (props: BackstoryElementProps) => {
}; };
return ( return (
<Box className="JobManagement" <Box sx={{
sx={{ minHeight: '100vh',
background: "white", backgroundColor: 'background.default',
p: 0, pt: { xs: 2, sm: 3 }
}}> }}>
{selectedJob === null && renderJobCreation()} {selectedJob === null && renderJobCreation()}
</Box> </Box>

View File

@ -322,7 +322,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Typography> </Typography>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
Backstory Generated Job Summary: Backstory Job Summary:
</Typography> </Typography>
<Typography variant="body1" component="h2"> <Typography variant="body1" component="h2">
{job.summary || "N/A"} {job.summary || "N/A"}
@ -333,7 +333,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexShrink: 1, flexDirection: "column" }}> <Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexShrink: 1, flexDirection: "column" }}>
<Typography variant="h6" component="h2"> <Typography variant="h6" component="h2">
Original Job Description: Job Description:
</Typography> </Typography>
<Paper sx={{ p: 2, maxHeight: "22rem" }}> <Paper sx={{ p: 2, maxHeight: "22rem" }}>
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}> <Scrollable sx={{ display: "flex", maxHeight: "100%" }}>

View File

@ -98,34 +98,26 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
className="BackstoryPageContainer" className="BackstoryPageContainer"
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "row", // Must be row; if column, the box will expand for all children
flexGrow: 1, flexGrow: 1,
p: "0 !important", // Let the first box use padding to offset main content p: { xs: 0, sm: 0.5 }, // Zero padding on mobile (xs), 0.5 on larger screens (sm and up)
m: "0 auto !important", m: "0 auto !important",
maxWidth: '1024px', //{ xs: '100%', md: '700px', lg: '1024px' }, maxWidth: '1024px', //{ xs: '100%', md: '700px', lg: '1024px' },
height: "100%", // Restrict to main-container's height
minHeight: 0,//"min-content", // Prevent flex overflow
...sx ...sx
}}>
<Box sx={{
display: "flex", p: { xs: 0, sm: 0.5 }, flexGrow: 1, minHeight: "min-content", // Prevent flex overflow
}}> }}>
<Paper <Paper
elevation={2} elevation={2}
sx={{ sx={{
display: "flex", display: "flex",
flexGrow: 1, flexGrow: 1,
m: 0,
p: 0.5, p: 0.5,
minHeight: "min-content", // Prevent flex overflow
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
borderRadius: 0.5, borderRadius: 0.5,
minHeight: '80vh',
maxWidth: '100%', maxWidth: '100%',
flexDirection: "column", flexDirection: "column",
}}> }}>
{children} {children}
</Paper> </Paper>
</Box>
</Container> </Container>
); );
} }

View File

@ -1,84 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { CandidateInfo } from 'components/CandidateInfo';
import { Candidate } from "types/types";
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void
};
const CandidatePicker = (props: CandidatePickerProps) => {
const { onSelect } = props;
const { apiClient, user } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const navigate = useNavigate();
const { setSnack } = props;
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
useEffect(() => {
if (candidates !== null) {
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;
});
setCandidates(candidates);
} catch (err) {
setSnack("" + err);
}
};
getCandidates();
}, [candidates, setSnack]);
return (
<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" }}>
{candidates?.map((u, i) =>
<Box key={`${u.username}`}
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
sx={{ cursor: "pointer" }}>
{selectedCandidate?.id === u.id &&
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: "#f0f0f0", "&:hover": { border: "2px solid orange" } }} candidate={u} />
}
{selectedCandidate?.id !== u.id &&
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} candidate={u} />
}
</Box>
)}
</Box>
</Box>
);
};
export {
CandidatePicker
};

View File

@ -1,10 +1,77 @@
import React, { } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { BackstoryPageProps } from '../components/BackstoryTab'; import { BackstoryPageProps } from '../components/BackstoryTab';
import { CandidatePicker } from 'components/ui/CandidatePicker'; import { CandidateInfo } from 'components/CandidateInfo';
import { Candidate, CandidateAI } from "../types/types";
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext';
const CandidateListingPage = (props: BackstoryPageProps) => { const CandidateListingPage = (props: BackstoryPageProps) => {
return <CandidatePicker {...props} />; const { apiClient, user } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const navigate = useNavigate();
const { setSnack } = props;
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
useEffect(() => {
if (candidates !== null) {
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;
});
setCandidates(candidates);
} catch (err) {
setSnack("" + err);
}
};
getCandidates();
}, [candidates, setSnack]);
return (
<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" }}>
{candidates?.map((u, i) =>
<Box key={`${u.username}`}
onClick={() => { setSelectedCandidate(u); navigate("/chat"); }}
sx={{ cursor: "pointer" }}>
{selectedCandidate?.id === u.id &&
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: "#f0f0f0", "&:hover": { border: "2px solid orange" } }} candidate={u} />
}
{selectedCandidate?.id !== u.id &&
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} candidate={u} />
}
</Box>
)}
</Box>
</Box>
);
}; };
export { export {

View File

@ -7,14 +7,25 @@ import {
Button, Button,
Typography, Typography,
Paper, Paper,
TextField,
Grid,
Card,
CardContent,
CardActionArea,
Avatar, Avatar,
Divider,
CircularProgress,
Container,
useTheme, useTheme,
Snackbar, Snackbar,
Alert, Alert,
} from '@mui/material'; } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import PersonIcon from '@mui/icons-material/Person'; 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 DescriptionIcon from '@mui/icons-material/Description';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { JobMatchAnalysis } from 'components/JobMatchAnalysis'; import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate } from "types/types"; import { Candidate } from "types/types";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -26,7 +37,6 @@ import { ComingSoon } from 'components/ui/ComingSoon';
import { JobManagement } from 'components/JobManagement'; import { JobManagement } from 'components/JobManagement';
import { LoginRequired } from 'components/ui/LoginRequired'; import { LoginRequired } from 'components/ui/LoginRequired';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
import { CandidatePicker } from 'components/ui/CandidatePicker';
// Main component // Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
@ -41,6 +51,44 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [analysisStarted, setAnalysisStarted] = useState(false); const [analysisStarted, setAnalysisStarted] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
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;
});
setCandidates(candidates);
} catch (err) {
setSnack("" + err);
}
};
getCandidates();
}, [candidates, setSnack]);
useEffect(() => {
if (selectedCandidate && activeStep === 0) {
setActiveStep(1);
}
}, [selectedCandidate, activeStep]);
useEffect(() => { useEffect(() => {
if (selectedJob && activeStep === 1) { if (selectedJob && activeStep === 1) {
@ -50,11 +98,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
// Steps in our process // Steps in our process
const steps = [ const steps = [
{ index: 0, label: 'Select Candidate', icon: <PersonIcon /> },
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> }, { index: 1, label: 'Job Selection', icon: <WorkIcon /> },
{ index: 2, label: 'Job Analysis', icon: <WorkIcon /> }, { index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> } { index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
]; ];
if (!selectedCandidate) {
steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> })
}
// Navigation handlers // Navigation handlers
const handleNext = () => { const handleNext = () => {
@ -76,38 +126,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
}; };
const handleBack = () => { const handleBack = () => {
console.log(activeStep);
if (activeStep === 1) {
setSelectedCandidate(null);
}
if (activeStep === 2) {
setSelectedJob(null);
}
setActiveStep((prevActiveStep) => prevActiveStep - 1); setActiveStep((prevActiveStep) => prevActiveStep - 1);
}; };
const moveToStep = (step: number) => { const handleReset = () => {
switch (step) { // setActiveStep(0);
case 0: /* Select candidate */
setSelectedCandidate(null);
setSelectedJob(null);
break;
case 1: /* Select Job */
setSelectedCandidate(null);
setSelectedJob(null);
break;
case 2: /* Job Analysis */
break;
case 3: /* Generate Resume */
break;
}
setActiveStep(step);
}
const onCandidateSelect = (candidate: Candidate) => {
setSelectedCandidate(candidate);
setActiveStep(1); setActiveStep(1);
} // setSelectedCandidate(null);
setSelectedJob(null);
// setJobTitle('');
// setJobLocation('');
setAnalysisStarted(false);
};
// Render function for the candidate selection step // Render function for the candidate selection step
const renderCandidateSelection = () => ( const renderCandidateSelection = () => (
@ -116,7 +146,77 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
Select a Candidate Select a Candidate
</Typography> </Typography>
<CandidatePicker onSelect={onCandidateSelect} {...backstoryProps} /> {/* <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> </Paper>
); );
@ -158,49 +258,30 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
} }
return ( return (
<Box sx={{ <Box sx={{ maxHeight: "100%", position: "relative", display: "flex", flexDirection: "column", overflow: "hidden" }}>
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> <Paper elevation={4} sx={{ m: 0, borderRadius: 0, mb: 1, p: 0 }}>{selectedCandidate && <CandidateInfo variant="small" candidate={selectedCandidate} sx={{ width: "100%" }} />}</Paper>
<Scrollable <Scrollable sx={{ display: "flex", flexGrow: 1, maxHeight: "calc(100dvh - 234px)", position: "relative" }}>
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" }}> <Box sx={{ display: "flex", justifyContent: "center" }}>
<Typography variant="subtitle1" color="text.secondary" gutterBottom> <Typography variant="subtitle1" color="text.secondary" gutterBottom>
Match candidates to job requirements with AI-powered analysis Match candidates to job requirements with AI-powered analysis
</Typography> </Typography>
</Box> </Box>
<Box sx={{ mt: 4, mb: 4 }}> <Box sx={{ mt: 4, mb: 4 }}>
<Stepper activeStep={activeStep} alternativeLabel> <Stepper activeStep={activeStep} alternativeLabel>
{steps.map((step, index) => ( {steps.map(step => (
<Step> <Step key={step.index}>
<StepLabel sx={{ cursor: "pointer" }} onClick={() => { moveToStep(index); }} <StepLabel slots={{
slots={{ stepIcon: () => (
stepIcon: () => ( <Avatar
<Avatar key={step.index} sx={{
sx={{ bgcolor: activeStep >= step.index ? theme.palette.primary.main : theme.palette.grey[300],
bgcolor: activeStep >= step.index ? theme.palette.primary.main : theme.palette.grey[300], color: 'white'
color: 'white' }}
}} >
> {step.icon}
{step.icon} </Avatar>
</Avatar> )
) }}
}}
> >
{step.label} {step.label}
</StepLabel> </StepLabel>
@ -213,7 +294,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
{activeStep === 1 && renderJobDescription()} {activeStep === 1 && renderJobDescription()}
{activeStep === 2 && renderAnalysis()} {activeStep === 2 && renderAnalysis()}
{activeStep === 3 && renderResume()} {activeStep === 3 && renderResume()}
</Scrollable>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button <Button
@ -227,7 +307,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ flex: '1 1 auto' }} /> <Box sx={{ flex: '1 1 auto' }} />
{activeStep === steps[steps.length - 1].index ? ( {activeStep === steps[steps.length - 1].index ? (
<Button onClick={() => { moveToStep(0) }} variant="outlined"> <Button onClick={handleReset} variant="outlined">
Start New Analysis Start New Analysis
</Button> </Button>
) : ( ) : (
@ -247,7 +327,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}> <Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}>
{error} {error}
</Alert> </Alert>
</Snackbar> </Snackbar>
</Scrollable>
</Box>); </Box>);
}; };