Updated How it Works

This commit is contained in:
James Ketr 2025-07-01 15:53:54 -07:00
parent aa6be077e6
commit 150228f83d
11 changed files with 83 additions and 36 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -293,6 +293,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
domain: requirements[i].domain,
};
} else {
if (firstRun) {
continue;
}
setSkillMatches(prev => {
const updated = [...prev];
updated[i] = { ...updated[i], status: 'pending' };
@ -353,10 +356,10 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setPercentage(0);
fetchMatchData(firstRun).then(() => {
setFirstRun(false);
setAnalyzing(false);
setStartAnalysis(false);
});
setFirstRun(false);
}, [
job,
startAnalysis,
@ -507,7 +510,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
onClick={beginAnalysis}
variant="contained"
>
{analyzing ? 'Assessment in Progress' : 'Assess Unknown Skills'}
{analyzing ? 'Assessment in Progress' : 'Analyze Waiting Skills'}
</Button>
</Box>

View File

@ -29,6 +29,7 @@ import waitPng from 'assets/wait.png';
import finalResumePng from 'assets/final-resume.png';
import { Beta } from 'components/ui/Beta';
import { Quote } from '@uiw/react-json-view';
// Styled components matching HomePage patterns
const HeroSection = styled(Box)(({ theme }) => ({
@ -77,8 +78,8 @@ const ImageContainer = styled(Box)(({ theme }) => ({
const steps = [
'Select Job Analysis',
'Choose a Job',
'Select a Candidate',
'Choose a Job',
'Start Assessment',
'Review Results',
'Generate Resume',
@ -367,7 +368,7 @@ const HowItWorks: React.FC = () => {
subtitle="Navigate to the main feature"
icon={<AssessmentIcon sx={{ color: 'action.active' }} />}
description={[
"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.",
"Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate the requirements for a job and perform a Backstory assisted evaluation of a candidate, or yourself!",
]}
imageSrc={selectJobAnalysisPng}
imageAlt="Select Job Analysis from menu"
@ -375,7 +376,27 @@ const HowItWorks: React.FC = () => {
</Container>
</StepSection>
{/* Step 2: Select a Job */}
{/* Step 2: Select a Candidate */}
<StepSection>
<Container>
<StepContent
stepNumber={3}
title="Select a Candidate"
subtitle="Choose from available candidate profiles"
icon={<PersonIcon sx={{ color: 'action.active' }} />}
description={[
'First, select a candidate. If you create an account and are logged in, it will default to selecting you. 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.',
'Once you\'ve selected a candidate, click "Next".',
]}
imageSrc={selectACandidatePng}
imageAlt="Select a candidate from the available profiles"
note="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."
reversed={true}
/>
</Container>
</StepSection>
{/* Step 3: Select a Job */}
<StepSection>
<Container>
<StepContent
@ -384,30 +405,13 @@ const HowItWorks: React.FC = () => {
subtitle="Pick from existing job postings"
icon={<WorkIcon sx={{ color: 'action.active' }} />}
description={[
'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.',
'After selecting a candidate, 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.',
'Clicking a job will give you more details about it.',
'When you\'re happy with your selection, click "Next".',
]}
imageSrc={selectAJobPng}
imageAlt="Select a job from the available options"
note="You can create your own job postings once you create an account. Until then, you need to select one that already exists."
reversed={true}
/>
</Container>
</StepSection>
{/* Step 3: Select a Candidate */}
<StepSection>
<Container>
<StepContent
stepNumber={3}
title="Select a Candidate"
subtitle="Choose from available profiles"
icon={<PersonIcon sx={{ color: 'action.active' }} />}
description={[
'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.',
]}
imageSrc={selectACandidatePng}
imageAlt="Select a candidate from the available profiles"
note="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."
/>
</Container>
</StepSection>
@ -423,7 +427,7 @@ const HowItWorks: React.FC = () => {
description={[
'After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will take each of requirements extracted from the Job and match it against information 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" button.',
'To see that in action, click the "Start Skill Assessment" button. Backstory will save the results, only regenerating the information if the candidate updates their uploaded content.',
]}
imageSrc={selectStartAnalysisPng}
imageAlt="Start the skill assessment process"
@ -443,6 +447,7 @@ const HowItWorks: React.FC = () => {
description={[
'Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it collates information about the candidate. As Backstory performs its magic, 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.',
'After looking at the results, you can click "Next" to proceed to the final step--resume generation.',
]}
imageSrc={waitPng}
imageAlt="Wait for the analysis to complete and review results"
@ -461,6 +466,7 @@ const HowItWorks: React.FC = () => {
description={[
'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.",
'After the initial resume is generated, if you are logged in, you can select "Save Resume and Edit" to take you to the resume editor.',
]}
imageSrc={finalResumePng}
imageAlt="Generated custom resume tailored to the job"

View File

@ -78,7 +78,7 @@ const capitalize = (str: string): string => {
// Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
const theme = useTheme();
const { user, guest } = useAuth();
const { user, guest, apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { selectedJob, setSelectedJob } = useSelectedJob();
@ -95,6 +95,22 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
const [activeStep, setActiveStep] = useState<number>(user === null ? 0 : 1);
const maxStep = 4;
useEffect(() => {
if (selectedCandidate === null && user !== null) {
apiClient
.getCandidate(user.id || '')
.then((candidate: Candidate | null) => {
if (candidate) {
setSelectedCandidate(candidate);
}
})
.catch((err: Error) => {
console.error('Error fetching candidate:', err);
setError('Failed to fetch candidate information.');
});
}
}, [user, apiClient, selectedCandidate, setSelectedCandidate]);
const getMissingStepRequirement = useCallback(
(step: number) => {
switch (step) {
@ -393,7 +409,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
}}
>
<Box sx={{ mb: 1, justifyContent: 'center' }}>Candidate Selection</Box>
{user !== null && (
{selectedCandidate !== null && (
<Box
sx={{
justifySelf: 'flex-start',
@ -403,9 +419,23 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
width: '100%',
}}
>
<Avatar
src={
selectedCandidate.profileImage
? `/api/1.0/candidates/profile/${selectedCandidate.username}`
: ''
}
alt={`${selectedCandidate.fullName}'s profile`}
sx={{
alignSelf: 'flex-start',
width: 32,
height: 32,
border: '2px solid #e0e0e0',
}}
/>
<Box sx={{ flexDirection: 'column', textAlign: 'left' }}>
<Box>Name</Box>
<Box sx={{ fontWeight: 'normal' }}>{user?.fullName}</Box>
<Box sx={{ fontWeight: 'normal' }}>{selectedCandidate?.fullName}</Box>
</Box>
</Box>
)}

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/backend/models.py
// Generated on: 2025-07-01T21:24:10.743667
// Generated on: 2025-07-01T22:28:07.615325
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================
@ -53,7 +53,7 @@ export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert";
export type SkillStatus = "pending" | "complete" | "waiting" | "error";
export type SkillStrength = "strong" | "moderate" | "weak" | "none";
export type SkillStrength = "strong" | "moderate" | "weak" | "none" | "unknown";
export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
@ -1041,7 +1041,7 @@ export interface SkillAssessment {
skill: string;
skillModified?: string;
evidenceFound: boolean;
evidenceStrength: "strong" | "moderate" | "weak" | "none";
evidenceStrength: "strong" | "moderate" | "weak" | "none" | "unknown";
assessment: string;
description: string;
evidenceDetails?: Array<EvidenceDetail>;

View File

@ -97,6 +97,7 @@ class SkillStrength(str, Enum):
MODERATE = "moderate"
WEAK = "weak"
NONE = "none"
UNKNOWN = "unknown"
class EvidenceDetail(BaseModel):

View File

@ -59,6 +59,7 @@ from models import (
RagContentMetadata,
RagContentResponse,
SkillAssessment,
SkillStrength,
UserType,
)
from utils.dependencies import (
@ -1440,7 +1441,7 @@ async def get_candidate_chat_summary(
@router.post("/job-analysis")
async def post_job_analysis(
request: JobAnalysis = Body(...),
current_user=Depends(get_current_user),
current_user=Depends(get_current_user_or_guest),
database: RedisDatabase = Depends(get_database),
):
"""Get chat activity summary for a candidate"""
@ -1467,7 +1468,6 @@ async def post_job_analysis(
job = Job.model_validate(job_data)
uninitalized = False
requirements = get_requirements_list(job)
logger.info(
@ -1486,8 +1486,15 @@ async def post_job_analysis(
logger.info(f"💾 No cached skill match data: {cache_key}, {candidate.id}, {skill}")
continue
else:
logger.info(f"✅ Assessment found for {candidate.username} skill {assessment.skill}: {cache_key}")
matched_skills.append(assessment)
if assessment.evidence_strength != SkillStrength.UNKNOWN:
logger.info(
f"✅ Assessment found for {candidate.username} skill {assessment.skill}: {assessment.evidence_strength}"
)
matched_skills.append(assessment)
else:
logger.info(
f"❌ Assessment for {candidate.username} skill {assessment.skill} is unknown, skipping."
)
request.skills = matched_skills
return create_success_response(request.model_dump(by_alias=True))