From d2d0bb29acc9d17774cd2083e76764eba32f0dc3 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Wed, 11 Jun 2025 15:58:40 -0700 Subject: [PATCH] Beta seems functional --- frontend/src/config/navigationConfig.tsx | 2 +- frontend/src/pages/HowItWorks.tsx | 12 ++------ frontend/src/pages/candidate/Profile.tsx | 39 +++++++++++++++++++++--- frontend/src/types/types.ts | 5 ++- src/backend/main.py | 4 ++- src/backend/models.py | 2 ++ 6 files changed, 46 insertions(+), 18 deletions(-) diff --git a/frontend/src/config/navigationConfig.tsx b/frontend/src/config/navigationConfig.tsx index 1847a2f..b981915 100644 --- a/frontend/src/config/navigationConfig.tsx +++ b/frontend/src/config/navigationConfig.tsx @@ -220,7 +220,7 @@ export const navigationConfig: NavigationConfig = { // }, { id: "candidate-settings", - label: "Settings", + label: "System Information", path: "/candidate/settings", icon: , component: , diff --git a/frontend/src/pages/HowItWorks.tsx b/frontend/src/pages/HowItWorks.tsx index 47e9004..b28db17 100644 --- a/frontend/src/pages/HowItWorks.tsx +++ b/frontend/src/pages/HowItWorks.tsx @@ -307,14 +307,6 @@ const HowItWorks: React.FC = () => { Here are your steps from zero-to-hero to see Backstory in action - @@ -395,7 +387,7 @@ const HowItWorks: React.FC = () => { subtitle="Begin the AI analysis" icon={} description={[ - "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.", + "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." ]} @@ -415,7 +407,7 @@ const HowItWorks: React.FC = () => { subtitle="Watch the magic happen" icon={} description={[ - "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 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." ]} imageSrc={waitPng} diff --git a/frontend/src/pages/candidate/Profile.tsx b/frontend/src/pages/candidate/Profile.tsx index 36e935f..363676c 100644 --- a/frontend/src/pages/candidate/Profile.tsx +++ b/frontend/src/pages/candidate/Profile.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import CheckIcon from '@mui/icons-material/Check'; import { Box, Button, @@ -27,7 +28,9 @@ import { FormControl, InputLabel, Switch, - FormControlLabel + FormControlLabel, + ToggleButton, + Checkbox } from '@mui/material'; import { styled } from '@mui/material/styles'; import { @@ -100,7 +103,8 @@ const CandidateProfile: React.FC = (props: BackstoryPageProp const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const { user, updateUserData, apiClient } = useAuth(); - + const [isPublic, setIsPublic] = React.useState(true); + // Check if user is a candidate const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null; @@ -120,7 +124,7 @@ const CandidateProfile: React.FC = (props: BackstoryPageProp // Form data state const [formData, setFormData] = useState>({}); - const [profileImage, setProfileImage] = useState(null); + const [profileImage, setProfileImage] = useState(''); // Dialog states const [skillDialog, setSkillDialog] = useState(false); @@ -146,7 +150,12 @@ const CandidateProfile: React.FC = (props: BackstoryPageProp useEffect(() => { if (candidate) { setFormData(candidate); - setProfileImage(candidate.profileImage || null); + if (candidate.profileImage) { + setProfileImage(`/api/1.0/candidates/profile/${candidate.username}`); + } else { + setProfileImage(''); + } + console.log({ isPublic: candidate.isPublic }); } }, [candidate]); @@ -177,6 +186,7 @@ const CandidateProfile: React.FC = (props: BackstoryPageProp const handleImageUpload = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { if (await apiClient.uploadCandidateProfile(e.target.files[0])) { + setProfileImage(URL.createObjectURL(e.target.files[0])); candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, ''); console.log(`Set profile image to: ${candidate.profileImage}`); updateUserData(candidate); @@ -265,7 +275,7 @@ const CandidateProfile: React.FC = (props: BackstoryPageProp = (props: BackstoryPageProp + + {editMode.basic ? ( + ) => handleInputChange('isPublic', event.target.checked)} />} + label={ + formData.isPublic ? + 'Your account will appear in candidate lists and searches on Backstory.' : + `Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.` + } /> + ) : (<>{ + candidate.isPublic ? + 'Your account will appear in candidate lists and searches on Backstory.' : + `Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.` + })} + + + {editMode.basic ? ( ; rags?: Array; ragContentSize: number; + isPublic: boolean; } export interface CandidateAI { @@ -241,6 +242,7 @@ export interface CandidateAI { jobApplications?: Array; rags?: Array; ragContentSize: number; + isPublic: boolean; isAI: boolean; age?: number; gender?: "female" | "male"; @@ -634,6 +636,7 @@ export interface Guest { ipAddress?: string; userAgent?: string; ragContentSize: number; + isPublic: boolean; } export interface GuestCleanupRequest { diff --git a/src/backend/main.py b/src/backend/main.py index 1180a86..2f1e6e7 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -3121,6 +3121,7 @@ async def get_candidates( sortBy: Optional[str] = Query(None, alias="sortBy"), sortOrder: str = Query("desc", pattern="^(asc|desc)$", alias="sortOrder"), filters: Optional[str] = Query(None), + current_user = Depends(get_current_user), database: RedisDatabase = Depends(get_database) ): """Get paginated list of candidates""" @@ -3133,7 +3134,8 @@ async def get_candidates( # Get all candidates from Redis all_candidates_data = await database.get_all_candidates() candidates_list = [Candidate.model_validate(data) if not data.get("is_AI") else CandidateAI.model_validate(data) for data in all_candidates_data.values()] - + candidates_list = [c for c in candidates_list if c.is_public or c.id == current_user.id] + paginated_candidates, total = filter_and_paginate( candidates_list, page, limit, sortBy, sortOrder, filter_dict ) diff --git a/src/backend/models.py b/src/backend/models.py index 484e6a0..a44dd35 100644 --- a/src/backend/models.py +++ b/src/backend/models.py @@ -604,6 +604,7 @@ class Candidate(BaseUser): job_applications: Optional[List["JobApplication"]] = Field(None, alias="jobApplications") rags: List[RagEntry] = Field(default_factory=list) rag_content_size : int = 0 + is_public: bool = Field(default=True, alias="isPublic") class CandidateAI(Candidate): user_type: UserType = Field(UserType.CANDIDATE, alias="userType") @@ -634,6 +635,7 @@ class Guest(BaseUser): created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt") user_agent: Optional[str] = Field(None, alias="userAgent") rag_content_size: int = 0 + is_public: bool = Field(default=False, alias="isPublic") model_config = { "populate_by_name": True, # Allow both field names and aliases "use_enum_values": True # Use enum values instead of names