Beta seems functional

This commit is contained in:
James Ketr 2025-06-11 15:58:40 -07:00
parent 74201d0a71
commit d2d0bb29ac
6 changed files with 46 additions and 18 deletions

View File

@ -220,7 +220,7 @@ export const navigationConfig: NavigationConfig = {
// }, // },
{ {
id: "candidate-settings", id: "candidate-settings",
label: "Settings", label: "System Information",
path: "/candidate/settings", path: "/candidate/settings",
icon: <SettingsIcon />, icon: <SettingsIcon />,
component: <Settings />, component: <Settings />,

View File

@ -307,14 +307,6 @@ const HowItWorks: React.FC = () => {
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}> <Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Here are your steps from zero-to-hero to see Backstory in action Here are your steps from zero-to-hero to see Backstory in action
</Typography> </Typography>
<Chip
label="Beta Version"
sx={{
backgroundColor: 'action.active',
color: 'background.paper',
fontWeight: 'bold'
}}
/>
</Box> </Box>
</Container> </Container>
</HeroSection> </HeroSection>
@ -395,7 +387,7 @@ const HowItWorks: React.FC = () => {
subtitle="Begin the AI analysis" subtitle="Begin the AI analysis"
icon={<PlayArrowIcon sx={{ color: 'action.active' }} />} icon={<PlayArrowIcon sx={{ color: 'action.active' }} />}
description={[ 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.", "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."
]} ]}
@ -415,7 +407,7 @@ const HowItWorks: React.FC = () => {
subtitle="Watch the magic happen" subtitle="Watch the magic happen"
icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />} icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />}
description={[ 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." "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} imageSrc={waitPng}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import CheckIcon from '@mui/icons-material/Check';
import { import {
Box, Box,
Button, Button,
@ -27,7 +28,9 @@ import {
FormControl, FormControl,
InputLabel, InputLabel,
Switch, Switch,
FormControlLabel FormControlLabel,
ToggleButton,
Checkbox
} from '@mui/material'; } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { import {
@ -100,7 +103,8 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, updateUserData, apiClient } = useAuth(); const { user, updateUserData, apiClient } = useAuth();
const [isPublic, setIsPublic] = React.useState(true);
// Check if user is a candidate // Check if user is a candidate
const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null; const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null;
@ -120,7 +124,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
// Form data state // Form data state
const [formData, setFormData] = useState<Partial<Types.Candidate>>({}); const [formData, setFormData] = useState<Partial<Types.Candidate>>({});
const [profileImage, setProfileImage] = useState<string | null>(null); const [profileImage, setProfileImage] = useState<string>('');
// Dialog states // Dialog states
const [skillDialog, setSkillDialog] = useState(false); const [skillDialog, setSkillDialog] = useState(false);
@ -146,7 +150,12 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
useEffect(() => { useEffect(() => {
if (candidate) { if (candidate) {
setFormData(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]); }, [candidate]);
@ -177,6 +186,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
if (await apiClient.uploadCandidateProfile(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(/^.*\./, ''); candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, '');
console.log(`Set profile image to: ${candidate.profileImage}`); console.log(`Set profile image to: ${candidate.profileImage}`);
updateUserData(candidate); updateUserData(candidate);
@ -265,7 +275,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
<Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}> <Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Avatar <Avatar
src={profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''} src={profileImage}
sx={{ sx={{
width: { xs: 80, sm: 120 }, width: { xs: 80, sm: 120 },
height: { xs: 80, sm: 120 }, height: { xs: 80, sm: 120 },
@ -298,6 +308,25 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
</Box> </Box>
</Box> </Box>
<Box className="entry" sx={{ display: 'flex', justifyContent: 'center', alignContent: 'center', gap: 1, "& span": { mb: 0 } }}>
{editMode.basic ? (
<FormControlLabel sx={{ display: 'flex' }} control={
<Switch
checked={formData.isPublic}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => 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}'.`
}</>)}
</Box>
<Box className="entry"> <Box className="entry">
{editMode.basic ? ( {editMode.basic ? (
<TextField <TextField

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/backend/models.py // Source: src/backend/models.py
// Generated on: 2025-06-10T17:14:56.968033 // Generated on: 2025-06-11T22:14:30.373041
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -207,6 +207,7 @@ export interface Candidate {
jobApplications?: Array<JobApplication>; jobApplications?: Array<JobApplication>;
rags?: Array<RagEntry>; rags?: Array<RagEntry>;
ragContentSize: number; ragContentSize: number;
isPublic: boolean;
} }
export interface CandidateAI { export interface CandidateAI {
@ -241,6 +242,7 @@ export interface CandidateAI {
jobApplications?: Array<any>; jobApplications?: Array<any>;
rags?: Array<RagEntry>; rags?: Array<RagEntry>;
ragContentSize: number; ragContentSize: number;
isPublic: boolean;
isAI: boolean; isAI: boolean;
age?: number; age?: number;
gender?: "female" | "male"; gender?: "female" | "male";
@ -634,6 +636,7 @@ export interface Guest {
ipAddress?: string; ipAddress?: string;
userAgent?: string; userAgent?: string;
ragContentSize: number; ragContentSize: number;
isPublic: boolean;
} }
export interface GuestCleanupRequest { export interface GuestCleanupRequest {

View File

@ -3121,6 +3121,7 @@ async def get_candidates(
sortBy: Optional[str] = Query(None, alias="sortBy"), sortBy: Optional[str] = Query(None, alias="sortBy"),
sortOrder: str = Query("desc", pattern="^(asc|desc)$", alias="sortOrder"), sortOrder: str = Query("desc", pattern="^(asc|desc)$", alias="sortOrder"),
filters: Optional[str] = Query(None), filters: Optional[str] = Query(None),
current_user = Depends(get_current_user),
database: RedisDatabase = Depends(get_database) database: RedisDatabase = Depends(get_database)
): ):
"""Get paginated list of candidates""" """Get paginated list of candidates"""
@ -3133,7 +3134,8 @@ async def get_candidates(
# Get all candidates from Redis # Get all candidates from Redis
all_candidates_data = await database.get_all_candidates() 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 = [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( paginated_candidates, total = filter_and_paginate(
candidates_list, page, limit, sortBy, sortOrder, filter_dict candidates_list, page, limit, sortBy, sortOrder, filter_dict
) )

View File

@ -604,6 +604,7 @@ class Candidate(BaseUser):
job_applications: Optional[List["JobApplication"]] = Field(None, alias="jobApplications") job_applications: Optional[List["JobApplication"]] = Field(None, alias="jobApplications")
rags: List[RagEntry] = Field(default_factory=list) rags: List[RagEntry] = Field(default_factory=list)
rag_content_size : int = 0 rag_content_size : int = 0
is_public: bool = Field(default=True, alias="isPublic")
class CandidateAI(Candidate): class CandidateAI(Candidate):
user_type: UserType = Field(UserType.CANDIDATE, alias="userType") 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") created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), alias="createdAt")
user_agent: Optional[str] = Field(None, alias="userAgent") user_agent: Optional[str] = Field(None, alias="userAgent")
rag_content_size: int = 0 rag_content_size: int = 0
is_public: bool = Field(default=False, alias="isPublic")
model_config = { model_config = {
"populate_by_name": True, # Allow both field names and aliases "populate_by_name": True, # Allow both field names and aliases
"use_enum_values": True # Use enum values instead of names "use_enum_values": True # Use enum values instead of names