Full initial flow for guest working
This commit is contained in:
parent
3a21f2e510
commit
bb4017b835
BIN
frontend/public/final-resume.png
Executable file
BIN
frontend/public/final-resume.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
BIN
frontend/public/select-a-candidate.png
Executable file
BIN
frontend/public/select-a-candidate.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
BIN
frontend/public/select-a-job.png
Executable file
BIN
frontend/public/select-a-job.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
frontend/public/select-job-analysis.png
Executable file
BIN
frontend/public/select-job-analysis.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
frontend/public/select-start-analysis.png
Executable file
BIN
frontend/public/select-start-analysis.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
BIN
frontend/public/wait.png
Executable file
BIN
frontend/public/wait.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -95,13 +95,6 @@ const JobCreator = (props: JobCreatorProps) => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<LoginRequired asset="job creation" />
|
||||
);
|
||||
}
|
||||
|
||||
const jobStatusHandlers = {
|
||||
onStatus: (status: Types.ChatMessageStatus) => {
|
||||
console.log('status:', status.content);
|
||||
|
@ -32,10 +32,12 @@ import { useAppState } from 'hooks/GlobalContext';
|
||||
import * as Types from 'types/types';
|
||||
import JsonView from '@uiw/react-json-view';
|
||||
import { VectorVisualizer } from './VectorVisualizer';
|
||||
import { JobInfo } from './ui/JobInfo';
|
||||
|
||||
interface JobAnalysisProps extends BackstoryPageProps {
|
||||
job: Job;
|
||||
candidate: Candidate;
|
||||
variant?: "small" | "normal";
|
||||
onAnalysisComplete: (skills: SkillAssessment[]) => void;
|
||||
}
|
||||
|
||||
@ -54,6 +56,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
job,
|
||||
candidate,
|
||||
onAnalysisComplete,
|
||||
variant = "normal",
|
||||
} = props
|
||||
const { apiClient } = useAuth();
|
||||
const { setSnack } = useAppState();
|
||||
@ -69,6 +72,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
|
||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
||||
const [matchStatus, setMatchStatus] = useState<string>('');
|
||||
const [matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null);
|
||||
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
@ -133,6 +137,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
|
||||
const skillMatchHandlers = {
|
||||
onStatus: (status: Types.ChatMessageStatus) => {
|
||||
setMatchStatusType(status.activity);
|
||||
setMatchStatus(status.content.toLowerCase());
|
||||
},
|
||||
};
|
||||
@ -238,67 +243,30 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexGrow: 1, flexDirection: "column" }}>
|
||||
<Typography variant="h6" component="h2">
|
||||
Company:
|
||||
</Typography>
|
||||
<Typography variant="body1" component="h2">
|
||||
{job.company || "N/A"}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{variant !== "small" &&
|
||||
<JobInfo job={job} variant="normal" />
|
||||
}
|
||||
|
||||
<Typography variant="h6" component="h2">
|
||||
Job Title:
|
||||
</Typography>
|
||||
<Typography variant="body1" component="h2">
|
||||
{job.title || "N/A"}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" component="h2">
|
||||
Backstory Generated Job Summary:
|
||||
</Typography>
|
||||
<Typography variant="body1" component="h2">
|
||||
{job.summary || "N/A"}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">Job ID: {job.id}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexShrink: 1, flexDirection: "column" }}>
|
||||
<Typography variant="h6" component="h2">
|
||||
Original Job Description:
|
||||
</Typography>
|
||||
<Paper sx={{ p: 2, maxHeight: "22rem" }}>
|
||||
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}>
|
||||
<StyledMarkdown content={job.description} />
|
||||
</Scrollable>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Grid size={{ xs: 12 }} sx={{ mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}>
|
||||
{<Button disabled={analyzing || startAnalysis} onClick={beginAnalysis} variant="contained">Start Skill Assessment</Button>}
|
||||
{overallScore !== 0 && <>
|
||||
<Box sx={{ display: 'flex', flexDirection: "row", alignItems: 'center', mb: 2, gap: 1, justifyContent: "space-between" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", flexGrow: 1 }}>
|
||||
{overallScore !== 0 && <>
|
||||
<Typography variant="h5" component="h2" sx={{ mr: 2 }}>
|
||||
Overall Match:
|
||||
</Typography>
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
mr: 2
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
mr: 2
|
||||
}}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={overallScore}
|
||||
size={60}
|
||||
thickness={5}
|
||||
sx={{
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={overallScore}
|
||||
size={60}
|
||||
thickness={5}
|
||||
sx={{
|
||||
color: getMatchColor(overallScore),
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
@ -317,23 +285,24 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Chip
|
||||
<Chip
|
||||
label={
|
||||
overallScore >= 80 ? "Excellent Match" :
|
||||
overallScore >= 60 ? "Good Match" :
|
||||
overallScore >= 40 ? "Partial Match" : "Low Match"
|
||||
}
|
||||
sx={{
|
||||
overallScore >= 40 ? "Partial Match" : "Low Match"
|
||||
}
|
||||
sx={{
|
||||
bgcolor: getMatchColor(overallScore),
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</>}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</>}
|
||||
</Box>
|
||||
<Button sx={{ marginLeft: "auto" }} disabled={analyzing || startAnalysis} onClick={beginAnalysis} variant="contained">
|
||||
{analyzing ? "Assessment in Progress" : "Start Skill Assessment"}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{loadingRequirements ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
|
@ -373,7 +373,7 @@ const Message = (props: MessageProps) => {
|
||||
if (typeof (message.content) === "string") {
|
||||
content = message.content.trim();
|
||||
} else {
|
||||
console.error(`message content is not a string`);
|
||||
console.error(`message content is not a string, it is a ${typeof message.content}`);
|
||||
return (<></>)
|
||||
}
|
||||
|
||||
|
@ -30,16 +30,14 @@ const defaultMessage: Types.ChatMessageStatus = {
|
||||
|
||||
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
|
||||
const { job, candidate, skills, onComplete } = props;
|
||||
const { apiClient } = useAuth();
|
||||
const { apiClient, user } = useAuth();
|
||||
const [resume, setResume] = useState<string>('');
|
||||
const [prompt, setPrompt] = useState<string>('');
|
||||
const [systemPrompt, setSystemPrompt] = useState<string>('');
|
||||
const [generating, setGenerating] = useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<Types.ChatMessageStatus | null>(null);
|
||||
const [tabValue, setTabValue] = useState<string>('resume');
|
||||
|
||||
|
||||
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
setTabValue(newValue);
|
||||
}
|
||||
@ -47,7 +45,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
|
||||
|
||||
const generateResumeHandlers = {
|
||||
onStatus: (status: Types.ChatMessageStatus) => {
|
||||
setStatusMessage({...defaultMessage, content: status.content.toLowerCase});
|
||||
setStatusMessage({ ...defaultMessage, content: status.content.toLowerCase() });
|
||||
},
|
||||
onStreaming: (chunk: Types.ChatMessageStreaming) =>{
|
||||
setResume(chunk.content);
|
||||
@ -82,20 +80,20 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||
<Tab sx={{display: systemPrompt ? "flex" : "none"}} value="system" icon={<TuneIcon />} label="System" />
|
||||
<Tab sx={{display: prompt ? "flex" : "none"}}value="prompt" icon={<InputIcon />} label="Prompt" />
|
||||
<Tab sx={{display: resume ? "flex" : "none"}}value="resume" icon={<ArticleIcon />} label="Resume" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
{ statusMessage && <Message message={statusMessage} />}
|
||||
<Paper elevation={3} sx={{ p: 3, m: 4, mt: 0 }}><Scrollable autoscroll sx={{display: "flex", flexGrow: 1}}>
|
||||
{ tabValue === 'system' && <pre>{systemPrompt}</pre> }
|
||||
{ tabValue === 'prompt' && <pre>{prompt}</pre> }
|
||||
{ tabValue === 'resume' && <StyledMarkdown content={resume} />}
|
||||
</Scrollable></Paper>
|
||||
</Box>
|
||||
{user?.isAdmin && <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||
<Tab sx={{ display: systemPrompt ? "flex" : "none" }} value="system" icon={<TuneIcon />} label="System" />
|
||||
<Tab sx={{ display: prompt ? "flex" : "none" }} value="prompt" icon={<InputIcon />} label="Prompt" />
|
||||
<Tab sx={{ display: resume ? "flex" : "none" }} value="resume" icon={<ArticleIcon />} label="Resume" />
|
||||
</Tabs>
|
||||
</Box>}
|
||||
{statusMessage && <Message message={statusMessage} />}
|
||||
<Paper elevation={3} sx={{ p: 3, m: 4, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1 }}>
|
||||
{tabValue === 'system' && <pre>{systemPrompt}</pre>}
|
||||
{tabValue === 'prompt' && <pre>{prompt}</pre>}
|
||||
{tabValue === 'resume' && <StyledMarkdown content={resume} />}
|
||||
</Scrollable></Paper>
|
||||
</Box>
|
||||
)
|
||||
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import { RefObject, useRef } from 'react';
|
||||
import { RefObject, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom';
|
||||
|
||||
interface ScrollableProps {
|
||||
@ -13,7 +13,7 @@ interface ScrollableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Scrollable = (props: ScrollableProps) => {
|
||||
const Scrollable = forwardRef((props: ScrollableProps, ref) => {
|
||||
const { sx, className, children, autoscroll, textFieldRef, fallbackThreshold = 0.33, contentUpdateTrigger } = props;
|
||||
// Create a default ref if textFieldRef is not provided
|
||||
const defaultTextFieldRef = useRef<HTMLElement | null>(null);
|
||||
@ -32,11 +32,11 @@ const Scrollable = (props: ScrollableProps) => {
|
||||
// backgroundColor: '#F5F5F5',
|
||||
...sx,
|
||||
}}
|
||||
ref={autoscroll !== undefined && autoscroll !== false ? scrollRef : undefined}
|
||||
ref={autoscroll !== undefined && autoscroll !== false ? scrollRef : ref}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export { useAutoScrollToBottom, Scrollable };
|
@ -48,114 +48,111 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
elevation={elevation}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
transition: 'all 0.3s ease',
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
...sx
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<CardContent sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||
{ai && <AIBanner />}
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 2 }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: "80px",
|
||||
maxWidth: "80px"
|
||||
}}>
|
||||
<Avatar
|
||||
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
||||
alt={`${candidate.fullName}'s profile`}
|
||||
{ai && <AIBanner />}
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "row" }}>
|
||||
<Avatar
|
||||
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
||||
alt={`${candidate.fullName}'s profile`}
|
||||
sx={{
|
||||
alignSelf: "flex-start",
|
||||
width: 80,
|
||||
height: 80,
|
||||
border: '2px solid #e0e0e0',
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{isAdmin && ai &&
|
||||
<DeleteConfirmation
|
||||
onDelete={() => { deleteCandidate(candidate.id); }}
|
||||
sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content", color: "red" }}
|
||||
action="delete"
|
||||
label="user"
|
||||
title="Delete AI user"
|
||||
icon=<DeleteIcon />
|
||||
message={`Are you sure you want to delete ${candidate.username}? This action cannot be undone.`}
|
||||
/>}
|
||||
/>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 10 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
mb: 1 }}>
|
||||
<Box sx={{ ml: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
mb: 1
|
||||
}}>
|
||||
<Box>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
alignItems: "left",
|
||||
gap: 1, "& > .MuiTypography-root": { m: 0 }
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
alignItems: "left",
|
||||
gap: 1, "& > .MuiTypography-root": { m: 0 }
|
||||
}}>
|
||||
{
|
||||
action !== '' &&
|
||||
<Typography variant="body1">{action}</Typography>
|
||||
action !== '' &&
|
||||
<Typography variant="body1">{action}</Typography>
|
||||
}
|
||||
<Typography variant="h5" component="h1"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap'
|
||||
{action === '' &&
|
||||
<Typography variant="h5" component="h1"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{candidate.fullName}
|
||||
</Typography>
|
||||
}
|
||||
</Box>
|
||||
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} >
|
||||
<Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link>
|
||||
<CopyBubble
|
||||
onClick={(event: any) => { event.stopPropagation() }}
|
||||
tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} />
|
||||
{isAdmin && ai &&
|
||||
<DeleteConfirmation
|
||||
onDelete={() => { deleteCandidate(candidate.id); }}
|
||||
sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content", color: "red" }}
|
||||
action="delete"
|
||||
label="user"
|
||||
title="Delete AI user"
|
||||
icon=<DeleteIcon />
|
||||
message={`Are you sure you want to delete ${candidate.username}? This action cannot be undone.`}
|
||||
/>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{candidate.description}
|
||||
</Typography>
|
||||
|
||||
{variant !== "small" && <>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{candidate.location &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Location:</strong> {candidate.location.city}, {candidate.location.state || candidate.location.country}
|
||||
</Typography>
|
||||
}
|
||||
{candidate.email &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Email:</strong> {candidate.email}
|
||||
</Typography>
|
||||
}
|
||||
{candidate.phone && <Typography variant="body2">
|
||||
<strong>Phone:</strong> {candidate.phone}
|
||||
{variant !== "small" && <>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{candidate.location &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Location:</strong> {candidate.location.city}, {candidate.location.state || candidate.location.country}
|
||||
</Typography>
|
||||
}
|
||||
</>}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
{candidate.email &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Email:</strong> {candidate.email}
|
||||
</Typography>
|
||||
}
|
||||
{candidate.phone && <Typography variant="body2">
|
||||
<strong>Phone:</strong> {candidate.phone}
|
||||
</Typography>
|
||||
}
|
||||
</>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -5,16 +5,16 @@ import Box from '@mui/material/Box';
|
||||
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { Candidate } from "types/types";
|
||||
import { Candidate, CandidateAI } from "types/types";
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
|
||||
interface CandidatePickerProps extends BackstoryElementProps {
|
||||
onSelect?: (candidate: Candidate) => void
|
||||
onSelect?: (candidate: Candidate) => void;
|
||||
};
|
||||
|
||||
const CandidatePicker = (props: CandidatePickerProps) => {
|
||||
const { onSelect } = props;
|
||||
const { onSelect, sx } = props;
|
||||
const { apiClient, user } = useAuth();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||
const navigate = useNavigate();
|
||||
@ -30,7 +30,12 @@ const CandidatePicker = (props: CandidatePickerProps) => {
|
||||
const results = await apiClient.getCandidates();
|
||||
const candidates: Candidate[] = results.data;
|
||||
candidates.sort((a, b) => {
|
||||
let result = a.lastName.localeCompare(b.lastName);
|
||||
const aIsAi = 'isAI' in a ? 1 : 0;
|
||||
const bIsAi = 'isAI' in b ? 1 : 0;
|
||||
let result = aIsAi - bIsAi;
|
||||
if (result === 0) {
|
||||
result = a.lastName.localeCompare(b.lastName);
|
||||
}
|
||||
if (result === 0) {
|
||||
result = a.firstName.localeCompare(b.firstName);
|
||||
}
|
||||
@ -49,7 +54,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
|
||||
}, [candidates, setSnack]);
|
||||
|
||||
return (
|
||||
<Box sx={{display: "flex", flexDirection: "column"}}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", ...sx }}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
|
||||
{candidates?.map((u, i) =>
|
||||
<Box key={`${u.username}`}
|
||||
|
@ -59,7 +59,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return <Box>No user loaded.</Box>;
|
||||
return <Box>No job provided.</Box>;
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@ -191,8 +191,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
elevation={elevation}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderColor: 'transparent',
|
||||
@ -204,26 +203,37 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<CardContent sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, p: 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||
<Box sx={{
|
||||
display: "flex", flexDirection: (isMobile || variant !== "small") ? "column" : "row",
|
||||
"& > div > div > :first-of-type": { fontWeight: "bold" },
|
||||
"& > div > div > :last-of-type": { mb: 0.75, mr: 1 }
|
||||
}}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}>
|
||||
{
|
||||
activeJob.title && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Title</Box>
|
||||
<Box>{activeJob.title}</Box>
|
||||
</Box>
|
||||
}
|
||||
{activeJob.company && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Company</Box>
|
||||
<Box>{activeJob.company}</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "75%" }}>
|
||||
{activeJob.summary && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Summary</Box>
|
||||
<Box>{activeJob.summary}</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{variant !== "small" && <>
|
||||
{activeJob.details &&
|
||||
{activeJob.details &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Location:</strong> {activeJob.details.location.city}, {activeJob.details.location.state || activeJob.details.location.country}
|
||||
</Typography>
|
||||
}
|
||||
{activeJob.title &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Title:</strong> {activeJob.title}
|
||||
</Typography>
|
||||
}
|
||||
{activeJob.company &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Company:</strong> {activeJob.company}
|
||||
</Typography>
|
||||
}
|
||||
{activeJob.summary && <Typography variant="body2">
|
||||
<strong>Summary:</strong> {activeJob.summary}
|
||||
</Typography>
|
||||
}
|
||||
{activeJob.owner && <Typography variant="body2">
|
||||
<strong>Created by:</strong> {activeJob.owner.fullName}
|
||||
@ -237,12 +247,11 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
<Typography variant="caption">Job ID: {job.id}</Typography>
|
||||
</>}
|
||||
|
||||
<Divider />
|
||||
{renderJobRequirements()}
|
||||
{variant !== 'small' && <><Divider />{renderJobRequirements()}</>}
|
||||
|
||||
</CardContent>
|
||||
</Box >
|
||||
{isAdmin &&
|
||||
<CardActions sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", pl: 1, pr: 1, gap: 1, alignContent: "center", height: "32px" }}>
|
||||
{(job.updatedAt && job.updatedAt.toISOString()) !== (activeJob.updatedAt && activeJob.updatedAt.toISOString()) &&
|
||||
<Tooltip title="Save Job">
|
||||
@ -290,9 +299,9 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
{adminStatus && <LinearProgress sx={{ mt: 1 }} />}
|
||||
</Box>
|
||||
}
|
||||
</CardActions>
|
||||
</Box>
|
||||
}
|
||||
</Card>
|
||||
</Box >
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -24,6 +24,7 @@ import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||
import { CandidatePicker } from 'components/ui/CandidatePicker';
|
||||
import { Scrollable } from 'components/Scrollable';
|
||||
|
||||
const defaultMessage: ChatMessage = {
|
||||
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user", metadata: null as any
|
||||
@ -186,55 +187,44 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={ref} sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}>
|
||||
<Box ref={ref}
|
||||
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={2} sx={{ m: 1, p: 1 }}>
|
||||
<CandidateInfo
|
||||
key={selectedCandidate.username}
|
||||
action={`Chat with Backstory about ${selectedCandidate.firstName}`}
|
||||
elevation={4}
|
||||
candidate={selectedCandidate}
|
||||
variant="small"
|
||||
sx={{ flexShrink: 0 }} // Prevent header from shrinking
|
||||
sx={{ flexShrink: 1, width: "100%", maxHeight: 0, minHeight: "min-content" }} // Prevent header from shrinking
|
||||
/>
|
||||
<Button onClick={() => { setSelectedCandidate(null); }} variant="contained">Change Candidates</Button>
|
||||
|
||||
<Button sx={{ maxWidth: "max-content" }} onClick={() => { setSelectedCandidate(null); }} variant="contained">Change Candidates</Button>
|
||||
</Paper>
|
||||
{/* Chat Interface */}
|
||||
<Paper
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
minHeight: 'max-content'
|
||||
}}
|
||||
>
|
||||
{/* Scrollable Messages Area */}
|
||||
{chatSession && <>
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 'max-content', // Important for flex child
|
||||
// Custom scrollbar styling
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
borderRadius: '4px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.grey[600],
|
||||
},
|
||||
},
|
||||
{/* Scrollable Messages Area */}
|
||||
{chatSession &&
|
||||
<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 */
|
||||
pt: 2,
|
||||
pl: 1,
|
||||
pr: 1,
|
||||
pb: 2,
|
||||
}}>
|
||||
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, }} />}
|
||||
{messages.map((message: ChatMessage) => (
|
||||
@ -263,12 +253,11 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
|
||||
</Box>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</Box>
|
||||
</>}
|
||||
</Paper>
|
||||
</Scrollable>
|
||||
}
|
||||
{selectedCandidate.questions?.length !== 0 && selectedCandidate.questions?.map(q => <BackstoryQuery question={q} />)}
|
||||
{/* Fixed Message Input */}
|
||||
<Box sx={{ display: 'flex', flexShrink: 0, gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
|
||||
<DeleteConfirmation
|
||||
onDelete={() => { chatSession && onDelete(chatSession); }}
|
||||
disabled={!chatSession}
|
||||
|
@ -1,22 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Box, Paper, Typography } from '@mui/material';
|
||||
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||
|
||||
const HowItWorks = () => {
|
||||
const content = `\
|
||||
Welcome to the Backstory Beta!
|
||||
|
||||
Here are your steps from zero-to-hero to see Backstory in action.
|
||||
|
||||

|
||||
|
||||
Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page,
|
||||
where you will get to evaluate a candidate for a selected job.
|
||||
|
||||
Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The
|
||||
requirements and information provided on Backstory are extracted from job postings that users have pasted as a
|
||||
job description or uploaded from a PDF. You can create your own job postings once you create an account. Until then,
|
||||
you need to select one that already exists.
|
||||
|
||||

|
||||
|
||||
Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several
|
||||
candidates which AI has generated. Each has a unique skillset and can be used to test out the system. If you create an account,
|
||||
you can opt-in to have your account show up for others to view as well, or keep it private for just your own resume
|
||||
generation and job research.
|
||||
|
||||

|
||||
|
||||
After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will
|
||||
take each of requirements that were extracted from the Job and match it against any information available about the selected candidate.
|
||||
This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to
|
||||
identify key elements from the candidate that pertain to a given skill and provides a graded response.
|
||||
|
||||

|
||||
|
||||
To see that in action, click the "Start Skill Assessment". Once you begin that action, the Start Skill Assessment button will grey out and
|
||||
the page will begin updating as it discovers information about the candidate. As it does its thing, you can monitor the progress and explore the different
|
||||
identified skills to see how or why a candidate does or does not have that skill.
|
||||
|
||||

|
||||
|
||||
Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics.
|
||||
|
||||
The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click "Next" to have Backstory
|
||||
generate the custom resume.
|
||||
|
||||

|
||||
|
||||
Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting.
|
||||
|
||||
You can then click the Copy button to copy the resume into your editor, adjust, and apply for your dream job!
|
||||
`;
|
||||
|
||||
return (<Paper sx={{ m: 1, p: 1 }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Box sx={{ display: "flex", alignContent: "center", verticalAlign: "center", flexDirection: "row" }}>
|
||||
<Typography>Job Description ⇒</Typography><BackstoryLogo /><Typography>⇒ (Company Info, Job Summary, Job Requirements) ⇒ <strong>Job</strong></Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignContent: "center", verticalAlign: "center", flexDirection: "row" }}>
|
||||
<Typography>User Content ⇒ </Typography><BackstoryLogo /><Typography>⇒ RAG Vector Database ⇒ <strong>Candidate</strong></Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignContent: "center", verticalAlign: "center", flexDirection: "row" }}>
|
||||
<Typography><strong>Job</strong> + <strong>Candidate</strong> ⇒ </Typography><BackstoryLogo /><Typography>⇒ <strong>Skill Match</strong></Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", alignContent: "center", verticalAlign: "center", flexDirection: "row" }}>
|
||||
<Typography><strong>Skill Match</strong> + <strong>Candidate</strong> ⇒ </Typography><BackstoryLogo /><Typography>⇒ <strong>Resume</strong></Typography>
|
||||
</Box>
|
||||
<StyledMarkdown content={content} />
|
||||
</Box>
|
||||
</Paper>);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Stepper,
|
||||
@ -13,6 +13,8 @@ import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Avatar,
|
||||
useMediaQuery,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add,
|
||||
@ -37,6 +39,7 @@ 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 (
|
||||
@ -63,108 +66,153 @@ function WorkAddIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
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: <WorkIcon /> },
|
||||
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
|
||||
{ requiredState: ['job', 'candidate'], title: 'Job Analysis', icon: <WorkIcon /> },
|
||||
{ requiredState: ['job', 'candidate', 'analysis'], title: 'Generated Resume', icon: <AssessmentIcon /> }
|
||||
].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<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const theme = useTheme();
|
||||
const { user, guest } = useAuth();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
// State management
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
|
||||
const [activeStep, setActiveStep] = useState<Step>(steps[0]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [jobTab, setJobTab] = useState<string>('load');
|
||||
const [skills, setSkills] = useState<SkillAssessment[] | null>(null)
|
||||
const [jobTab, setJobTab] = useState<string>('select');
|
||||
const [analysisState, setAnalysisState] = useState<AnalysisState | null>(null);
|
||||
const [canAdvance, setCanAdvance] = useState<boolean>(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(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 (!selectedCandidate) {
|
||||
if (activeStep !== 0) {
|
||||
setActiveStep(0);
|
||||
}
|
||||
} else if (!selectedJob) {
|
||||
if (activeStep !== 1) {
|
||||
setActiveStep(1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, [selectedCandidate, selectedJob, activeStep])
|
||||
}, [analysisState, selectedCandidate, selectedJob, setActiveStep, canAccessStep]);
|
||||
|
||||
// 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 /> }
|
||||
];
|
||||
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]);
|
||||
|
||||
// Navigation handlers
|
||||
const handleNext = () => {
|
||||
if (activeStep === 0 && !selectedCandidate) {
|
||||
setError('Please select a candidate before continuing.');
|
||||
if (activeStep.index === steps.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStep === 1 && !selectedJob) {
|
||||
setError('Please select a job before continuing.');
|
||||
return;
|
||||
const missing = canAccessStep(steps[activeStep.index + 1]);
|
||||
if (missing) {
|
||||
setError(`${capitalize(missing)} is necessary before continuing.`);
|
||||
return missing;
|
||||
}
|
||||
|
||||
if (activeStep === 2 && !skills) {
|
||||
setError('Skill assessment must be complete before continuing.');
|
||||
return;
|
||||
if (activeStep.index < steps.length - 1) {
|
||||
setActiveStep((prevActiveStep) => steps[prevActiveStep.index + 1]);
|
||||
}
|
||||
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
console.log(activeStep);
|
||||
if (activeStep === 1) {
|
||||
setSelectedCandidate(null);
|
||||
if (activeStep.index === 0) {
|
||||
return;
|
||||
}
|
||||
if (activeStep === 2) {
|
||||
setSelectedJob(null);
|
||||
}
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
|
||||
setActiveStep((prevActiveStep) => steps[prevActiveStep.index - 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;
|
||||
const missing = canAccessStep(steps[step]);
|
||||
if (missing) {
|
||||
setError(`${capitalize(missing)} is needed to access this step.`);
|
||||
return;
|
||||
}
|
||||
setActiveStep(step);
|
||||
setActiveStep(steps[step]);
|
||||
}
|
||||
|
||||
const onCandidateSelect = (candidate: Candidate) => {
|
||||
if (!analysisState) {
|
||||
return;
|
||||
}
|
||||
analysisState.candidate = candidate;
|
||||
setAnalysisState({ ...analysisState });
|
||||
setSelectedCandidate(candidate);
|
||||
setActiveStep(1);
|
||||
handleNext();
|
||||
}
|
||||
|
||||
const onJobSelect = (job: Job) => {
|
||||
setSelectedJob(job)
|
||||
setActiveStep(2);
|
||||
if (!analysisState) {
|
||||
return;
|
||||
}
|
||||
analysisState.job = job;
|
||||
setAnalysisState({ ...analysisState });
|
||||
setSelectedJob(job);
|
||||
handleNext();
|
||||
}
|
||||
|
||||
// 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>
|
||||
<CandidatePicker sx={{ pt: 1 }} onSelect={onCandidateSelect} />
|
||||
);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||
@ -173,19 +221,15 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
|
||||
// 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" />
|
||||
<Tab value='select' icon={<WorkOutline />} label="Select Job" />
|
||||
<Tab value='create' icon={<WorkAddIcon />} label="Create Job" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{jobTab === 'load' &&
|
||||
{jobTab === 'select' &&
|
||||
<JobPicker onSelect={onJobSelect} />
|
||||
}
|
||||
{jobTab === 'create' && user &&
|
||||
@ -201,32 +245,47 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
}
|
||||
|
||||
const onAnalysisComplete = (skills: SkillAssessment[]) => {
|
||||
setSkills(skills);
|
||||
if (!analysisState) {
|
||||
return;
|
||||
}
|
||||
analysisState.analysis = skills;
|
||||
setAnalysisState({ ...analysisState });
|
||||
};
|
||||
|
||||
// Render function for the analysis step
|
||||
const renderAnalysis = () => (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{selectedCandidate && selectedJob && (
|
||||
<JobMatchAnalysis
|
||||
job={selectedJob}
|
||||
candidate={selectedCandidate}
|
||||
onAnalysisComplete={onAnalysisComplete}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
const renderAnalysis = () => {
|
||||
if (!analysisState) {
|
||||
return;
|
||||
}
|
||||
if (!analysisState.job || !analysisState.candidate) {
|
||||
return <Box>{JSON.stringify({ job: analysisState.job, candidate: analysisState.candidate })}</Box>
|
||||
}
|
||||
return (<Box sx={{ mt: 3 }}>
|
||||
<JobMatchAnalysis
|
||||
variant="small"
|
||||
job={analysisState.job}
|
||||
candidate={analysisState.candidate}
|
||||
onAnalysisComplete={onAnalysisComplete}
|
||||
/>
|
||||
</Box>);
|
||||
};
|
||||
|
||||
const renderResume = () => (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{skills && selectedCandidate && selectedJob &&
|
||||
const renderResume = () => {
|
||||
if (!analysisState) {
|
||||
return;
|
||||
}
|
||||
if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (<Box sx={{ mt: 3 }}>
|
||||
<ResumeGenerator
|
||||
job={selectedJob}
|
||||
candidate={selectedCandidate}
|
||||
skills={skills}
|
||||
/>}
|
||||
</Box>
|
||||
);
|
||||
job={analysisState.job}
|
||||
candidate={analysisState.candidate}
|
||||
skills={analysisState.analysis}
|
||||
/>
|
||||
</Box>);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
@ -240,8 +299,48 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
},
|
||||
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, gap: 1 }}>
|
||||
<Stepper activeStep={activeStep.index} alternativeLabel sx={{ mt: 2, mb: 2 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Step>
|
||||
<StepLabel sx={{ cursor: "pointer" }} onClick={() => { moveToStep(index); }}
|
||||
slots={{
|
||||
stepIcon: () => (
|
||||
<Avatar key={step.index}
|
||||
sx={{
|
||||
bgcolor: activeStep.index >= step.index ? theme.palette.primary.main : theme.palette.grey[300],
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
{step.icon}
|
||||
</Avatar>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
{analysisState && analysisState.job &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
<Box sx={{ ml: 3, fontWeight: "bold" }}>Selected Job</Box>
|
||||
<JobInfo variant="small" job={analysisState.job} />
|
||||
</Box>
|
||||
}
|
||||
{isMobile && <Box sx={{ display: "flex", borderBottom: "1px solid grey" }} />}
|
||||
{!isMobile && <Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />}
|
||||
{analysisState && analysisState.candidate &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
<Box sx={{ ml: 3, fontWeight: "bold" }}>Selected Candidate</Box>
|
||||
<CandidateInfo variant="small" candidate={analysisState.candidate} sx={{}} />
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Paper>
|
||||
<Scrollable
|
||||
ref={scrollRef}
|
||||
sx={{
|
||||
position: "relative",
|
||||
maxHeight: "100%",
|
||||
@ -250,46 +349,16 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
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()}
|
||||
{activeStep.label === 'job-selection' && renderJobDescription()}
|
||||
{activeStep.label === 'select-candidate' && renderCandidateSelection()}
|
||||
{activeStep.label === 'job-analysis' && renderAnalysis()}
|
||||
{activeStep.label === 'generated-resume' && renderResume()}
|
||||
</Scrollable>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
disabled={activeStep === steps[0].index}
|
||||
disabled={activeStep.index === steps[0].index}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
@ -297,13 +366,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
</Button>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
|
||||
{activeStep === steps[steps.length - 1].index ? (
|
||||
<Button onClick={() => { moveToStep(0) }} variant="outlined">
|
||||
{activeStep.index === steps[steps.length - 1].index ? (
|
||||
<Button disabled={!canAdvance} onClick={() => { moveToStep(0) }} variant="outlined">
|
||||
Start New Analysis
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleNext} variant="contained">
|
||||
{activeStep === steps.length - 1 ? 'Done' : 'Next'}
|
||||
<Button disabled={!canAdvance} onClick={handleNext} variant="contained">
|
||||
{activeStep.index === steps.length - 1 ? 'Done' : 'Next'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
Loading…
x
Reference in New Issue
Block a user