diff --git a/frontend/public/final-resume.png b/frontend/public/final-resume.png new file mode 100755 index 0000000..3f5cc3e Binary files /dev/null and b/frontend/public/final-resume.png differ diff --git a/frontend/public/select-a-candidate.png b/frontend/public/select-a-candidate.png new file mode 100755 index 0000000..d9077c5 Binary files /dev/null and b/frontend/public/select-a-candidate.png differ diff --git a/frontend/public/select-a-job.png b/frontend/public/select-a-job.png new file mode 100755 index 0000000..0394e58 Binary files /dev/null and b/frontend/public/select-a-job.png differ diff --git a/frontend/public/select-job-analysis.png b/frontend/public/select-job-analysis.png new file mode 100755 index 0000000..3ee2971 Binary files /dev/null and b/frontend/public/select-job-analysis.png differ diff --git a/frontend/public/select-start-analysis.png b/frontend/public/select-start-analysis.png new file mode 100755 index 0000000..fd676e4 Binary files /dev/null and b/frontend/public/select-start-analysis.png differ diff --git a/frontend/public/wait.png b/frontend/public/wait.png new file mode 100755 index 0000000..11f123b Binary files /dev/null and b/frontend/public/wait.png differ diff --git a/frontend/src/components/JobCreator.tsx b/frontend/src/components/JobCreator.tsx index 62f1e88..d4874c0 100644 --- a/frontend/src/components/JobCreator.tsx +++ b/frontend/src/components/JobCreator.tsx @@ -95,13 +95,6 @@ const JobCreator = (props: JobCreatorProps) => { const fileInputRef = useRef(null); - - if (!user?.id) { - return ( - - ); - } - const jobStatusHandlers = { onStatus: (status: Types.ChatMessageStatus) => { console.log('status:', status.content); diff --git a/frontend/src/components/JobMatchAnalysis.tsx b/frontend/src/components/JobMatchAnalysis.tsx index d9df632..60430d0 100644 --- a/frontend/src/components/JobMatchAnalysis.tsx +++ b/frontend/src/components/JobMatchAnalysis.tsx @@ -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 = (props: JobAnalysisProps) = job, candidate, onAnalysisComplete, + variant = "normal", } = props const { apiClient } = useAuth(); const { setSnack } = useAppState(); @@ -69,6 +72,7 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = const [startAnalysis, setStartAnalysis] = useState(false); const [analyzing, setAnalyzing] = useState(false); const [matchStatus, setMatchStatus] = useState(''); + const [matchStatusType, setMatchStatusType] = useState(null); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -133,6 +137,7 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = const skillMatchHandlers = { onStatus: (status: Types.ChatMessageStatus) => { + setMatchStatusType(status.activity); setMatchStatus(status.content.toLowerCase()); }, }; @@ -238,67 +243,30 @@ const JobMatchAnalysis: React.FC = (props: JobAnalysisProps) = }; return ( - - - - - - - Company: - - - {job.company || "N/A"} - + + {variant !== "small" && + + } - - Job Title: - - - {job.title || "N/A"} - - - - Backstory Generated Job Summary: - - - {job.summary || "N/A"} - - - Job ID: {job.id} - - - - - Original Job Description: - - - - - - - - - - - - {} - {overallScore !== 0 && <> + + + {overallScore !== 0 && <> Overall Match: - - = (props: JobAnalysisProps) = - = 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' - }} + }} /> - } - - - - + } + + + {loadingRequirements ? ( diff --git a/frontend/src/components/Message.tsx b/frontend/src/components/Message.tsx index 6530710..6ab561f 100644 --- a/frontend/src/components/Message.tsx +++ b/frontend/src/components/Message.tsx @@ -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 (<>) } diff --git a/frontend/src/components/ResumeGenerator.tsx b/frontend/src/components/ResumeGenerator.tsx index 4d87d22..22d9c9c 100644 --- a/frontend/src/components/ResumeGenerator.tsx +++ b/frontend/src/components/ResumeGenerator.tsx @@ -30,16 +30,14 @@ const defaultMessage: Types.ChatMessageStatus = { const ResumeGenerator: React.FC = (props: ResumeGeneratorProps) => { const { job, candidate, skills, onComplete } = props; - const { apiClient } = useAuth(); + const { apiClient, user } = useAuth(); const [resume, setResume] = useState(''); const [prompt, setPrompt] = useState(''); const [systemPrompt, setSystemPrompt] = useState(''); const [generating, setGenerating] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [tabValue, setTabValue] = useState('resume'); - - - + const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { setTabValue(newValue); } @@ -47,7 +45,7 @@ const ResumeGenerator: React.FC = (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 = (props: ResumeGeneratorP display: "flex", flexDirection: "column", }}> - - - } label="System" /> - } label="Prompt" /> - } label="Resume" /> - - - { statusMessage && } - - { tabValue === 'system' &&
{systemPrompt}
} - { tabValue === 'prompt' &&
{prompt}
} - { tabValue === 'resume' && } -
-
+ {user?.isAdmin && + + } label="System" /> + } label="Prompt" /> + } label="Resume" /> + + } + {statusMessage && } + + {tabValue === 'system' &&
{systemPrompt}
} + {tabValue === 'prompt' &&
{prompt}
} + {tabValue === 'resume' && } +
+ ) }; diff --git a/frontend/src/components/Scrollable.tsx b/frontend/src/components/Scrollable.tsx index 067a925..7cb952a 100644 --- a/frontend/src/components/Scrollable.tsx +++ b/frontend/src/components/Scrollable.tsx @@ -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(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} ); -}; +}); export { useAutoScrollToBottom, Scrollable }; \ No newline at end of file diff --git a/frontend/src/components/ui/CandidateInfo.tsx b/frontend/src/components/ui/CandidateInfo.tsx index e16faf7..ef35099 100644 --- a/frontend/src/components/ui/CandidateInfo.tsx +++ b/frontend/src/components/ui/CandidateInfo.tsx @@ -48,114 +48,111 @@ const CandidateInfo: React.FC = (props: CandidateInfoProps) } return ( - - - {ai && } - - - - } + + + - - {isAdmin && ai && - { deleteCandidate(candidate.id); }} - sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content", color: "red" }} - action="delete" - label="user" - title="Delete AI user" - icon= - message={`Are you sure you want to delete ${candidate.username}? This action cannot be undone.`} - />} + /> - - + + - .MuiTypography-root": { m: 0 } + .MuiTypography-root": { m: 0 } }}> { - action !== '' && - {action} + action !== '' && + {action} } - {candidate.fullName} + } {`/u/${candidate.username}`} { event.stopPropagation() }} tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} /> + {isAdmin && ai && + { deleteCandidate(candidate.id); }} + sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content", color: "red" }} + action="delete" + label="user" + title="Delete AI user" + icon= + message={`Are you sure you want to delete ${candidate.username}? This action cannot be undone.`} + />} - + {candidate.description} - {variant !== "small" && <> - - - {candidate.location && - - Location: {candidate.location.city}, {candidate.location.state || candidate.location.country} - - } - {candidate.email && - - Email: {candidate.email} - - } - {candidate.phone && - Phone: {candidate.phone} + {variant !== "small" && <> + + + {candidate.location && + + Location: {candidate.location.city}, {candidate.location.state || candidate.location.country} - } - } - - - - + } + {candidate.email && + + Email: {candidate.email} + + } + {candidate.phone && + Phone: {candidate.phone} + + } + } + + + ); }; diff --git a/frontend/src/components/ui/CandidatePicker.tsx b/frontend/src/components/ui/CandidatePicker.tsx index 35e20c9..53b1120 100644 --- a/frontend/src/components/ui/CandidatePicker.tsx +++ b/frontend/src/components/ui/CandidatePicker.tsx @@ -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 ( - + {candidates?.map((u, i) => = (props: JobInfoProps) => { } if (!job) { - return No user loaded.; + return No job provided.; } const handleSave = async () => { @@ -191,8 +191,7 @@ const JobInfo: React.FC = (props: JobInfoProps) => { }; return ( - = (props: JobInfoProps) => { }} {...rest} > - + + div > div > :first-of-type": { fontWeight: "bold" }, + "& > div > div > :last-of-type": { mb: 0.75, mr: 1 } + }}> + + { + activeJob.title && + Title + {activeJob.title} + + } + {activeJob.company && + Company + {activeJob.company} + } + + + {activeJob.summary && + Summary + {activeJob.summary} + } + + + {variant !== "small" && <> - {activeJob.details && + {activeJob.details && Location: {activeJob.details.location.city}, {activeJob.details.location.state || activeJob.details.location.country} - } - {activeJob.title && - - Title: {activeJob.title} - - } - {activeJob.company && - - Company: {activeJob.company} - - } - {activeJob.summary && - Summary: {activeJob.summary} - } {activeJob.owner && Created by: {activeJob.owner.fullName} @@ -237,12 +247,11 @@ const JobInfo: React.FC = (props: JobInfoProps) => { Job ID: {job.id} } - - {renderJobRequirements()} + {variant !== 'small' && <>{renderJobRequirements()}} - + {isAdmin && - + {(job.updatedAt && job.updatedAt.toISOString()) !== (activeJob.updatedAt && activeJob.updatedAt.toISOString()) && @@ -290,9 +299,9 @@ const JobInfo: React.FC = (props: JobInfoProps) => { {adminStatus && } } - + } - + ); }; diff --git a/frontend/src/pages/CandidateChatPage.tsx b/frontend/src/pages/CandidateChatPage.tsx index bebc583..3fcb514 100644 --- a/frontend/src/pages/CandidateChatPage.tsx +++ b/frontend/src/pages/CandidateChatPage.tsx @@ -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((pr }; return ( - + *:not(.Scrollable)": { + flexShrink: 0, /* Prevent shrinking */ + }, + position: "relative", + }}> + - - + + {/* Chat Interface */} - - {/* Scrollable Messages Area */} - {chatSession && <> - {messages.length === 0 && } {messages.map((message: ChatMessage) => ( @@ -263,12 +253,11 @@ const CandidateChatPage = forwardRef((pr )}
- - } - + + } {selectedCandidate.questions?.length !== 0 && selectedCandidate.questions?.map(q => )} {/* Fixed Message Input */} - + { chatSession && onDelete(chatSession); }} disabled={!chatSession} diff --git a/frontend/src/pages/HowItWorks.tsx b/frontend/src/pages/HowItWorks.tsx index d9cfb96..3a63f36 100644 --- a/frontend/src/pages/HowItWorks.tsx +++ b/frontend/src/pages/HowItWorks.tsx @@ -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](/select-job-analysis.png) + +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. + +![select-a-job](/select-a-job.png) + +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. + +![select-a-candidate](/select-a-candidate.png) + +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. + +![select-start-analysis](/select-start-analysis.png) + +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. + +![Wait](/wait.png) + +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. + +![final-resume](/final-resume.png) + +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 ( - - Job Description ⇒⇒ (Company Info, Job Summary, Job Requirements) ⇒ Job - - - User Content ⇒ ⇒ RAG Vector Database ⇒ Candidate - - - Job + CandidateSkill Match - - - Skill Match + CandidateResume - + ); } diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index e68d629..f0a9987 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -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: }, + { requiredState: ['job'], title: 'Select Candidate', icon: }, + { requiredState: ['job', 'candidate'], title: 'Job Analysis', icon: }, + { requiredState: ['job', 'candidate', 'analysis'], title: 'Generated Resume', icon: } +].map((item, index) => { return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') } }); + +const capitalize = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1); +} + // Main component const JobAnalysisPage: React.FC = (props: BackstoryPageProps) => { const theme = useTheme(); const { user, guest } = useAuth(); - const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate() + const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedJob, setSelectedJob } = useSelectedJob(); - // State management - const [activeStep, setActiveStep] = useState(0); + + const [activeStep, setActiveStep] = useState(steps[0]); const [error, setError] = useState(null); - const [jobTab, setJobTab] = useState('load'); - const [skills, setSkills] = useState(null) + const [jobTab, setJobTab] = useState('select'); + const [analysisState, setAnalysisState] = useState(null); + const [canAdvance, setCanAdvance] = useState(false); + const scrollRef = useRef(null); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const canAccessStep = useCallback((step: Step) => { + if (!analysisState) { + return; + } + const missing = step.requiredState.find(f => !(analysisState as any)[f]) + return missing; + }, [analysisState]); + useEffect(() => { - if (!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: }, - { index: 1, label: 'Job Selection', icon: }, - { index: 2, label: 'Job Analysis', icon: }, - { index: 3, label: 'Generated Resume', icon: } - ]; + 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 = () => ( - - - Select a Candidate - - - - + ); const handleTabChange = (event: React.SyntheticEvent, value: string) => { @@ -173,19 +221,15 @@ const JobAnalysisPage: React.FC = (props: BackstoryPageProps // Render function for the job description step const renderJobDescription = () => { - if (!selectedCandidate) { - return; - } - return ( - } label="Load" /> - } label="Create" /> + } label="Select Job" /> + } label="Create Job" /> - {jobTab === 'load' && + {jobTab === 'select' && } {jobTab === 'create' && user && @@ -201,32 +245,47 @@ const JobAnalysisPage: React.FC = (props: BackstoryPageProps } const onAnalysisComplete = (skills: SkillAssessment[]) => { - setSkills(skills); + if (!analysisState) { + return; + } + analysisState.analysis = skills; + setAnalysisState({ ...analysisState }); }; // Render function for the analysis step - const renderAnalysis = () => ( - - {selectedCandidate && selectedJob && ( - - )} - - ); + const renderAnalysis = () => { + if (!analysisState) { + return; + } + if (!analysisState.job || !analysisState.candidate) { + return {JSON.stringify({ job: analysisState.job, candidate: analysisState.candidate })} + } + return ( + + ); + }; - const renderResume = () => ( - - {skills && selectedCandidate && selectedJob && + const renderResume = () => { + if (!analysisState) { + return; + } + if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) { + return <>; + } + + return ( } - - ); + job={analysisState.job} + candidate={analysisState.candidate} + skills={analysisState.analysis} + /> + ); + }; return ( = (props: BackstoryPageProps }, position: "relative", }}> - {selectedCandidate && } + + + {steps.map((step, index) => ( + + { moveToStep(index); }} + slots={{ + stepIcon: () => ( + = step.index ? theme.palette.primary.main : theme.palette.grey[300], + color: 'white' + }} + > + {step.icon} + + ) + }} + > + {step.title} + + + ))} + + + {analysisState && analysisState.job && + + Selected Job + + + } + {isMobile && } + {!isMobile && } + {analysisState && analysisState.candidate && + + Selected Candidate + + + } + + = (props: BackstoryPageProps flex: 1, /* Take remaining space in some-container */ overflowY: "auto", /* Scroll if content overflows */ }}> - - - Match candidates to job requirements with AI-powered analysis - - - - - {steps.map((step, index) => ( - - { moveToStep(index); }} - slots={{ - stepIcon: () => ( - = step.index ? theme.palette.primary.main : theme.palette.grey[300], - color: 'white' - }} - > - {step.icon} - - ) - }} - > - {step.label} - - - ))} - - - - {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()} - {activeStep === steps[steps.length - 1].index ? ( - ) : ( - )}